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,167 @@
# HTTP Output Plugin
This plugin writes metrics to a HTTP endpoint using one of the supported
[data formats][data_formats]. For data formats supporting batching, metrics are
sent in batches by default.
⭐ Telegraf v1.7.0
🏷️ applications
💻 all
[data_formats]: /docs/DATA_FORMATS_OUTPUT.md
## 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
## Secret-store support
This plugin supports secrets from secret-stores for the `username`, `password`
`headers`, and `cookie_auth_headers` option.
See the [secret-store documentation][SECRETSTORE] for more details on how
to use them.
[SECRETSTORE]: ../../../docs/CONFIGURATION.md#secret-store-secrets
## Configuration
```toml @sample.conf
# A plugin that can transmit metrics over HTTP
[[outputs.http]]
## URL is the address to send metrics to
url = "http://127.0.0.1:8080/telegraf"
## Timeout for HTTP message
# timeout = "5s"
## HTTP method, one of: "POST" or "PUT" or "PATCH"
# method = "POST"
## HTTP Basic Auth credentials
# username = "username"
# password = "pa$$word"
## OAuth2 Client Credentials Grant
# client_id = "clientid"
# client_secret = "secret"
# token_url = "https://indentityprovider/oauth2/v1/token"
# audience = ""
# scopes = ["urn:opc:idm:__myscopes__"]
## Goole API Auth
# google_application_credentials = "/etc/telegraf/example_secret.json"
## HTTP Proxy support
# use_system_proxy = false
# http_proxy_url = ""
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
## Optional Cookie authentication
# cookie_auth_url = "https://localhost/authMe"
# cookie_auth_method = "POST"
# cookie_auth_username = "username"
# cookie_auth_password = "pa$$word"
# cookie_auth_headers = '{"Content-Type": "application/json", "X-MY-HEADER":"hello"}'
# cookie_auth_body = '{"username": "user", "password": "pa$$word", "authenticate": "me"}'
## cookie_auth_renewal not set or set to "0" will auth once and never renew the cookie
# cookie_auth_renewal = "5m"
## Data format to output.
## Each data format has it's own unique set of configuration options, read
## more about them here:
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md
# data_format = "influx"
## Use batch serialization format (default) instead of line based format.
## Batch format is more efficient and should be used unless line based
## format is really needed.
# use_batch_format = true
## HTTP Content-Encoding for write request body, can be set to "gzip" to
## compress body or "identity" to apply no encoding.
# content_encoding = "identity"
## MaxIdleConns controls the maximum number of idle (keep-alive)
## connections across all hosts. Zero means no limit.
# max_idle_conn = 0
## MaxIdleConnsPerHost, if non-zero, controls the maximum idle
## (keep-alive) connections to keep per-host. If zero,
## DefaultMaxIdleConnsPerHost is used(2).
# max_idle_conn_per_host = 2
## Idle (keep-alive) connection timeout.
## Maximum amount of time before idle connection is closed.
## Zero means no limit.
# idle_conn_timeout = 0
## Amazon Region
#region = "us-east-1"
## Amazon Credentials
## Amazon Credentials are not built unless the following aws_service
## setting is set to a non-empty string. It may need to match the name of
## the service output to as well
#aws_service = "execute-api"
## Credentials are loaded in the following order
## 1) Web identity provider credentials via STS if role_arn and web_identity_token_file are specified
## 2) Assumed credentials via STS if role_arn is specified
## 3) explicit credentials from 'access_key' and 'secret_key'
## 4) shared profile from 'profile'
## 5) environment variables
## 6) shared credentials file
## 7) EC2 Instance Profile
#access_key = ""
#secret_key = ""
#token = ""
#role_arn = ""
#web_identity_token_file = ""
#role_session_name = ""
#profile = ""
#shared_credential_file = ""
## Optional list of statuscodes (<200 or >300) upon which requests should not be retried
# non_retryable_statuscodes = [409, 413]
## NOTE: Due to the way TOML is parsed, tables must be at the END of the
## plugin definition, otherwise additional config options are read as part of
## the table
## Additional HTTP headers
# [outputs.http.headers]
# ## Should be set manually to "application/json" for json data_format
# Content-Type = "text/plain; charset=utf-8"
```
### Google API Auth
The `google_application_credentials` setting is used with Google Cloud APIs.
It specifies the json key file. To learn about creating Google service accounts,
consult Google's [oauth2 service account documentation][create_service_account].
An example use case is a metrics proxy deployed to Cloud Run. In this example,
the service account must have the "run.routes.invoke" permission.
[create_service_account]: https://cloud.google.com/docs/authentication/production#create_service_account
### Optional Cookie Authentication Settings
The optional Cookie Authentication Settings will retrieve a cookie from the
given authorization endpoint, and use it in subsequent API requests. This is
useful for services that do not provide OAuth or Basic Auth authentication,
e.g. the [Tesla Powerwall API][powerwall], which uses a Cookie Auth Body to
retrieve an authorization cookie. The Cookie Auth Renewal interval will renew
the authorization by retrieving a new cookie at the given interval.
[powerwall]: https://www.tesla.com/support/energy/powerwall/own/monitoring-from-home-network

View file

@ -0,0 +1,285 @@
//go:generate ../../../tools/readme_config_includer/generator
package http
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
_ "embed"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
aws_signer "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"golang.org/x/oauth2"
"google.golang.org/api/idtoken"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal"
common_aws "github.com/influxdata/telegraf/plugins/common/aws"
common_http "github.com/influxdata/telegraf/plugins/common/http"
"github.com/influxdata/telegraf/plugins/outputs"
)
//go:embed sample.conf
var sampleConfig string
const (
maxErrMsgLen = 1024
defaultURL = "http://127.0.0.1:8080/telegraf"
)
const (
defaultContentType = "text/plain; charset=utf-8"
defaultMethod = http.MethodPost
defaultUseBatchFormat = true
)
type HTTP struct {
URL string `toml:"url"`
Method string `toml:"method"`
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
Headers map[string]*config.Secret `toml:"headers"`
ContentEncoding string `toml:"content_encoding"`
UseBatchFormat bool `toml:"use_batch_format"`
AwsService string `toml:"aws_service"`
NonRetryableStatusCodes []int `toml:"non_retryable_statuscodes"`
common_http.HTTPClientConfig
Log telegraf.Logger `toml:"-"`
client *http.Client
serializer telegraf.Serializer
awsCfg *aws.Config
common_aws.CredentialConfig
// Google API Auth
CredentialsFile string `toml:"google_application_credentials"`
oauth2Token *oauth2.Token
}
func (*HTTP) SampleConfig() string {
return sampleConfig
}
func (h *HTTP) SetSerializer(serializer telegraf.Serializer) {
h.serializer = serializer
}
func (h *HTTP) Connect() error {
if h.AwsService != "" {
cfg, err := h.CredentialConfig.Credentials()
if err == nil {
h.awsCfg = &cfg
}
}
if h.Method == "" {
h.Method = http.MethodPost
}
h.Method = strings.ToUpper(h.Method)
if h.Method != http.MethodPost && h.Method != http.MethodPut && h.Method != http.MethodPatch {
return fmt.Errorf("invalid method [%s] %s", h.URL, h.Method)
}
ctx := context.Background()
client, err := h.HTTPClientConfig.CreateClient(ctx, h.Log)
if err != nil {
return err
}
h.client = client
return nil
}
func (h *HTTP) Close() error {
if h.client != nil {
h.client.CloseIdleConnections()
}
return nil
}
func (h *HTTP) Write(metrics []telegraf.Metric) error {
if h.UseBatchFormat {
reqBody, err := h.serializer.SerializeBatch(metrics)
if err != nil {
return err
}
return h.writeMetric(reqBody)
}
for _, metric := range metrics {
reqBody, err := h.serializer.Serialize(metric)
if err != nil {
return err
}
if err := h.writeMetric(reqBody); err != nil {
return err
}
}
return nil
}
func (h *HTTP) writeMetric(reqBody []byte) error {
var reqBodyBuffer io.Reader = bytes.NewBuffer(reqBody)
var err error
if h.ContentEncoding == "gzip" {
rc := internal.CompressWithGzip(reqBodyBuffer)
defer rc.Close()
reqBodyBuffer = rc
}
var payloadHash *string
if h.awsCfg != nil {
// We need a local copy of the full buffer, the signature scheme requires a sha256 of the request body.
buf := new(bytes.Buffer)
_, err = io.Copy(buf, reqBodyBuffer)
if err != nil {
return err
}
sum := sha256.Sum256(buf.Bytes())
reqBodyBuffer = buf
// sha256 is hex encoded
hash := hex.EncodeToString(sum[:])
payloadHash = &hash
}
req, err := http.NewRequest(h.Method, h.URL, reqBodyBuffer)
if err != nil {
return err
}
if h.awsCfg != nil {
signer := aws_signer.NewSigner()
ctx := context.Background()
credentials, err := h.awsCfg.Credentials.Retrieve(ctx)
if err != nil {
return err
}
err = signer.SignHTTP(ctx, credentials, req, *payloadHash, h.AwsService, h.Region, time.Now().UTC())
if err != nil {
return err
}
}
if !h.Username.Empty() || !h.Password.Empty() {
username, err := h.Username.Get()
if err != nil {
return fmt.Errorf("getting username failed: %w", err)
}
password, err := h.Password.Get()
if err != nil {
username.Destroy()
return fmt.Errorf("getting password failed: %w", err)
}
req.SetBasicAuth(username.String(), password.String())
username.Destroy()
password.Destroy()
}
// google api auth
if h.CredentialsFile != "" {
token, err := h.getAccessToken(context.Background(), h.URL)
if err != nil {
return err
}
token.SetAuthHeader(req)
}
req.Header.Set("User-Agent", internal.ProductToken())
req.Header.Set("Content-Type", defaultContentType)
if h.ContentEncoding == "gzip" {
req.Header.Set("Content-Encoding", "gzip")
}
for k, v := range h.Headers {
secret, err := v.Get()
if err != nil {
return err
}
headerVal := secret.String()
if strings.EqualFold(k, "host") {
req.Host = headerVal
}
req.Header.Set(k, headerVal)
secret.Destroy()
}
resp, err := h.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
for _, nonRetryableStatusCode := range h.NonRetryableStatusCodes {
if resp.StatusCode == nonRetryableStatusCode {
h.Log.Errorf("Received non-retryable status %v. Metrics are lost.", resp.StatusCode)
return nil
}
}
errorLine := ""
scanner := bufio.NewScanner(io.LimitReader(resp.Body, maxErrMsgLen))
if scanner.Scan() {
errorLine = scanner.Text()
}
return fmt.Errorf("when writing to [%s] received status code: %d. body: %s", h.URL, resp.StatusCode, errorLine)
}
_, err = io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("when writing to [%s] received error: %w", h.URL, err)
}
return nil
}
func init() {
outputs.Add("http", func() telegraf.Output {
return &HTTP{
Method: defaultMethod,
URL: defaultURL,
UseBatchFormat: defaultUseBatchFormat,
}
})
}
func (h *HTTP) getAccessToken(ctx context.Context, audience string) (*oauth2.Token, error) {
if h.oauth2Token.Valid() {
return h.oauth2Token, nil
}
ts, err := idtoken.NewTokenSource(ctx, audience, idtoken.WithCredentialsFile(h.CredentialsFile))
if err != nil {
return nil, fmt.Errorf("error creating oauth2 token source: %w", err)
}
token, err := ts.Token()
if err != nil {
return nil, fmt.Errorf("error fetching oauth2 token: %w", err)
}
h.oauth2Token = token
return token, nil
}

View file

@ -0,0 +1,819 @@
package http
import (
"compress/gzip"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"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"
common_aws "github.com/influxdata/telegraf/plugins/common/aws"
common_http "github.com/influxdata/telegraf/plugins/common/http"
"github.com/influxdata/telegraf/plugins/common/oauth"
"github.com/influxdata/telegraf/plugins/serializers/influx"
"github.com/influxdata/telegraf/plugins/serializers/json"
"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(n int) []telegraf.Metric {
m := make([]telegraf.Metric, n)
for n > 0 {
n--
m[n] = getMetric()
}
return m
}
func TestInvalidMethod(t *testing.T) {
plugin := &HTTP{
URL: "",
Method: http.MethodGet,
}
err := plugin.Connect()
require.Error(t, err)
}
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 *HTTP
expectedMethod string
connectError bool
}{
{
name: "default method is POST",
plugin: &HTTP{
URL: u.String(),
Method: defaultMethod,
},
expectedMethod: http.MethodPost,
},
{
name: "put is okay",
plugin: &HTTP{
URL: u.String(),
Method: http.MethodPut,
},
expectedMethod: http.MethodPut,
},
{
name: "get is invalid",
plugin: &HTTP{
URL: u.String(),
Method: http.MethodGet,
},
connectError: true,
},
{
name: "method is case insensitive",
plugin: &HTTP{
URL: u.String(),
Method: "poST",
},
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 := &influx.Serializer{}
require.NoError(t, serializer.Init())
tt.plugin.SetSerializer(serializer)
err = tt.plugin.Connect()
if tt.connectError {
require.Error(t, err)
return
}
require.NoError(t, err)
err = tt.plugin.Write([]telegraf.Metric{getMetric()})
require.NoError(t, err)
})
}
}
func TestHTTPClientConfig(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 *HTTP
connectError bool
expectedMaxIdleConns int
expectedMaxIdleConnsPerHost int
}{
{
name: "With default client Config",
plugin: &HTTP{
URL: u.String(),
Method: defaultMethod,
HTTPClientConfig: common_http.HTTPClientConfig{
IdleConnTimeout: config.Duration(5 * time.Second),
},
},
expectedMaxIdleConns: 0,
expectedMaxIdleConnsPerHost: 0,
},
{
name: "With MaxIdleConns client Config",
plugin: &HTTP{
URL: u.String(),
Method: defaultMethod,
HTTPClientConfig: common_http.HTTPClientConfig{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: config.Duration(5 * time.Second),
},
},
expectedMaxIdleConns: 100,
expectedMaxIdleConnsPerHost: 100,
},
}
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(http.StatusOK)
})
serializer := &influx.Serializer{}
require.NoError(t, serializer.Init())
tt.plugin.SetSerializer(serializer)
err = tt.plugin.Connect()
if tt.connectError {
require.Error(t, err)
return
}
require.NoError(t, err)
tr := tt.plugin.client.Transport.(*http.Transport)
maxIdleConns, maxIdleConnsPerHost := tr.MaxIdleConns, tr.MaxIdleConnsPerHost
require.Equal(t, tt.expectedMaxIdleConns, maxIdleConns)
require.Equal(t, tt.expectedMaxIdleConnsPerHost, maxIdleConnsPerHost)
err = tt.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)
tests := []struct {
name string
plugin *HTTP
statusCode int
errFunc func(t *testing.T, err error)
}{
{
name: "success",
plugin: &HTTP{
URL: u.String(),
},
statusCode: http.StatusOK,
errFunc: func(t *testing.T, err error) {
require.NoError(t, err)
},
},
{
name: "1xx status is an error",
plugin: &HTTP{
URL: u.String(),
},
statusCode: http.StatusSwitchingProtocols,
errFunc: func(t *testing.T, err error) {
require.Error(t, err)
},
},
{
name: "3xx status is an error",
plugin: &HTTP{
URL: u.String(),
},
statusCode: http.StatusMultipleChoices,
errFunc: func(t *testing.T, err error) {
require.Error(t, err)
},
},
{
name: "4xx status is an error",
plugin: &HTTP{
URL: u.String(),
},
statusCode: http.StatusBadRequest,
errFunc: func(t *testing.T, err error) {
require.Error(t, err)
},
},
{
name: "Do not retry on configured non-retryable statuscode",
plugin: &HTTP{
URL: u.String(),
NonRetryableStatusCodes: []int{409},
},
statusCode: http.StatusConflict,
errFunc: func(t *testing.T, err error) {
require.NoError(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 := &influx.Serializer{}
require.NoError(t, serializer.Init())
tt.plugin.SetSerializer(serializer)
err = tt.plugin.Connect()
require.NoError(t, err)
tt.plugin.Log = testutil.Logger{}
err = tt.plugin.Write([]telegraf.Metric{getMetric()})
tt.errFunc(t, err)
})
}
}
func TestContentType(t *testing.T) {
ts := httptest.NewServer(http.NotFoundHandler())
defer ts.Close()
u, err := url.Parse("http://" + ts.Listener.Addr().String())
require.NoError(t, err)
headerSecret := config.NewSecret([]byte("application/json"))
tests := []struct {
name string
plugin *HTTP
expected string
}{
{
name: "default is text plain",
plugin: &HTTP{
URL: u.String(),
},
expected: defaultContentType,
},
{
name: "overwrite content_type",
plugin: &HTTP{
URL: u.String(),
Headers: map[string]*config.Secret{"Content-Type": &headerSecret},
},
expected: "application/json",
},
}
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 contentHeader := r.Header.Get("Content-Type"); contentHeader != tt.expected {
w.WriteHeader(http.StatusInternalServerError)
t.Errorf("Not equal, expected: %q, actual: %q", tt.expected, contentHeader)
return
}
w.WriteHeader(http.StatusOK)
})
serializer := &influx.Serializer{}
require.NoError(t, serializer.Init())
tt.plugin.SetSerializer(serializer)
err = tt.plugin.Connect()
require.NoError(t, err)
err = tt.plugin.Write([]telegraf.Metric{getMetric()})
require.NoError(t, err)
})
}
}
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 *HTTP
payload string
expected string
}{
{
name: "default is no content encoding",
plugin: &HTTP{
URL: u.String(),
},
expected: "",
},
{
name: "overwrite content_encoding",
plugin: &HTTP{
URL: u.String(),
ContentEncoding: "gzip",
},
expected: "gzip",
},
}
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 contentHeader := r.Header.Get("Content-Encoding"); contentHeader != tt.expected {
w.WriteHeader(http.StatusInternalServerError)
t.Errorf("Not equal, expected: %q, actual: %q", tt.expected, contentHeader)
return
}
body := r.Body
var err error
if r.Header.Get("Content-Encoding") == "gzip" {
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 !strings.Contains(string(payload), "cpu value=42") {
w.WriteHeader(http.StatusInternalServerError)
t.Errorf("'payload' should contain %q", "cpu value=42")
return
}
w.WriteHeader(http.StatusNoContent)
})
serializer := &influx.Serializer{}
require.NoError(t, serializer.Init())
tt.plugin.SetSerializer(serializer)
err = tt.plugin.Connect()
require.NoError(t, err)
err = tt.plugin.Write([]telegraf.Metric{getMetric()})
require.NoError(t, err)
})
}
}
func TestBasicAuth(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
username string
password string
}{
{
name: "default",
},
{
name: "username only",
username: "username",
},
{
name: "password only",
password: "pa$$word",
},
{
name: "username and password",
username: "username",
password: "pa$$word",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plugin := &HTTP{
URL: u.String(),
Username: config.NewSecret([]byte(tt.username)),
Password: config.NewSecret([]byte(tt.password)),
}
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, _ := r.BasicAuth()
if username != tt.username {
w.WriteHeader(http.StatusInternalServerError)
t.Errorf("Not equal, expected: %q, actual: %q", tt.username, username)
return
}
if password != tt.password {
w.WriteHeader(http.StatusInternalServerError)
t.Errorf("Not equal, expected: %q, actual: %q", tt.password, password)
return
}
w.WriteHeader(http.StatusOK)
})
serializer := &influx.Serializer{}
require.NoError(t, serializer.Init())
plugin.SetSerializer(serializer)
require.NoError(t, plugin.Connect())
require.NoError(t, plugin.Write([]telegraf.Metric{getMetric()}))
})
}
}
type TestHandlerFunc func(t *testing.T, w http.ResponseWriter, r *http.Request)
func TestOAuthClientCredentialsGrant(t *testing.T) {
ts := httptest.NewServer(http.NotFoundHandler())
defer ts.Close()
var token = "2YotnFZFEjr1zCsicMWpAA"
u, err := url.Parse("http://" + ts.Listener.Addr().String())
require.NoError(t, err)
tests := []struct {
name string
plugin *HTTP
tokenHandler TestHandlerFunc
handler TestHandlerFunc
}{
{
name: "no credentials",
plugin: &HTTP{
URL: u.String(),
},
handler: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
require.Empty(t, r.Header["Authorization"])
w.WriteHeader(http.StatusOK)
},
},
{
name: "success",
plugin: &HTTP{
URL: u.String() + "/write",
HTTPClientConfig: common_http.HTTPClientConfig{
OAuth2Config: oauth.OAuth2Config{
ClientID: "howdy",
ClientSecret: "secret",
TokenURL: u.String() + "/token",
Scopes: []string{"urn:opc:idm:__myscopes__"},
},
},
},
tokenHandler: func(t *testing.T, w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
values := url.Values{}
values.Add("access_token", token)
values.Add("token_type", "bearer")
values.Add("expires_in", "3600")
_, err = w.Write([]byte(values.Encode()))
require.NoError(t, err)
},
handler: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
require.Equal(t, []string{"Bearer " + token}, r.Header["Authorization"])
w.WriteHeader(http.StatusOK)
},
},
{
name: "audience",
plugin: &HTTP{
URL: u.String() + "/write",
HTTPClientConfig: common_http.HTTPClientConfig{
OAuth2Config: oauth.OAuth2Config{
ClientID: "howdy",
ClientSecret: "secret",
TokenURL: u.String() + "/token",
Scopes: []string{"urn:opc:idm:__myscopes__"},
Audience: "audience",
},
},
},
tokenHandler: func(t *testing.T, w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
values := url.Values{}
values.Add("access_token", token)
values.Add("token_type", "bearer")
values.Add("expires_in", "3600")
_, err = w.Write([]byte(values.Encode()))
require.NoError(t, err)
},
handler: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
require.Equal(t, []string{"Bearer " + token}, r.Header["Authorization"])
w.WriteHeader(http.StatusOK)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/write":
tt.handler(t, w, r)
case "/token":
tt.tokenHandler(t, w, r)
}
})
serializer := &influx.Serializer{}
require.NoError(t, serializer.Init())
tt.plugin.SetSerializer(serializer)
err = tt.plugin.Connect()
require.NoError(t, err)
err = tt.plugin.Write([]telegraf.Metric{getMetric()})
require.NoError(t, err)
})
}
}
func TestOAuthAuthorizationCodeGrant(t *testing.T) {
ts := httptest.NewServer(http.NotFoundHandler())
defer ts.Close()
u, err := url.Parse("http://" + ts.Listener.Addr().String())
require.NoError(t, err)
tmpDir := t.TempDir()
tmpFile, err := os.CreateTemp(tmpDir, "test_key_file")
require.NoError(t, err)
tmpTokenURI := u.String() + "/token"
data := []byte(
fmt.Sprintf(
"{\n \"type\": \"service_account\",\n \"project_id\": \"my-project\",\n "+
"\"private_key_id\": \"223423436436453645363456\",\n \"private_key\": "+
"\"-----BEGIN PRIVATE KEY-----\\nMIICXAIBAAKBgQDX7Plvu0MJtA9TrusYtQnAogsdiYJZd9wfFIjH5FxE3SWJ4KAIE+yRWRqcqX8XnpieQLaNsfXhDPWLkWngTDydk4NO/"+
"jlAQk0e6+9+NeiZ2ViIHmtXERb9CyiiWUmo+YCd69lhzSEIMK9EPBSDHQTgQMtEfGak03G5rx3MCakE1QIDAQABAoGAOjRU4Lt3zKvO3d3u3ZAfet+zY1jn3DolCfO9EzUJcj6ymc"+
"IFIWhNgrikJcrCyZkkxrPnAbcQ8oNNxTuDcMTcKZbnyUnlQj5NtVuty5Q+zgf3/Q2pRhaE+TwrpOJ+ETtVp9R/PrPN2NC5wPo289fPNWFYkd4DPbdWZp5AJHz1XYECQQD3kKpinJx"+
"MYp9FQ1Qj1OkxGln0KPgdqRYjjW/rXI4/hUodfg+xXWHPFSGj3AgEjQIvuengbOAeH3qowF1uxVTlAkEA30hXM3EbboMCDQzNRNkkV9EiZ0MZXhj1aIGl+sQZOmOeFdcdjGkDdsA4"+
"2nmaYqXCD9KAvc+S/tGJaa0Qg0VhMQJAb2+TAqh0Qn3yK39PFIH2JcAy1ZDLfq5p5L75rfwPm9AnuHbSIYhjSo+8gMG+ai3+2fTZrcfUajrJP8S3SfFRcQJBANQQPOHatxcKzlPeq"+
"MaPBXlyY553mAxK4CnVmPLGdL+EBYzwtlu5EVUj09uMSxkOHXYxk5yzHQVvtXbsrBZBOsECQBJLlkMjJmXrIIdLPmHQWL3bm9MMg1PqzupSEwz6cyrGuIIm/X91pDyxCHaKYWp38F"+
"XBkYAgohI8ow5/sgRvU5w=\\n-----END PRIVATE KEY-----\\n\",\n "+
"\"client_email\": \"test-service-account-email@example.iam.gserviceaccount.com\",\n \"client_id\": \"110300009813738675309\",\n "+
"\"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n \"token_uri\": \"%s\",\n "+
"\"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n "+
"\"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/test-service-account-email@example.iam.gserviceaccount.com\"\n}",
tmpTokenURI,
),
)
_, err = tmpFile.Write(data)
require.NoError(t, err)
require.NoError(t, tmpFile.Close())
const token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg2NzUzMDliMjJiMDFiZTU2YzIxM2M5ODU0MGFiNTYzYmZmNWE1OGMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vMTI3LjAuMC4x" +
"OjU4MDI1LyIsImF6cCI6InRlc3Qtc2VydmljZS1hY2NvdW50LWVtYWlsQGV4YW1wbGUuY29tIiwiZW1haWwiOiJ0ZXN0LXNlcnZpY2UtYWNjb3VudC1lbWFpbEBleGFtcGxlLmNvbSIsImVtY" +
"WlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjk0NjY4NDgwMCwiaWF0Ijo5NDY2ODEyMDAsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMudGVzdC5jb20iLCJzdWIiOiIxMTAzMDAwMDk4MTM3Mzg2Nz" +
"UzMDkifQ.qi2LsXP2o6nl-rbYKUlHAgTBY0QoU7Nhty5NGR4GMdc8OoGEPW-vlD0WBSaKSr11vyFcIO4ftFDWXElo9Ut-AIQPKVxinsjHIU2-LoIATgI1kyifFLyU_pBecwcI4CIXEcDK5wEk" +
"fonWFSkyDZHBeZFKbJXlQXtxj0OHvQ-DEEepXLuKY6v3s4U6GyD9_ppYUy6gzDZPYUbfPfgxCj_Jbv6qkLU0DiZ7F5-do6X6n-qkpgCRLTGHcY__rn8oe8_pSimsyJEeY49ZQ5lj4mXkVCwgL" +
"9bvL1_eW1p6sgbHaBnPKVPbM7S1_cBmzgSonm__qWyZUxfDgNdigtNsvzBQTg"
tests := []struct {
name string
plugin *HTTP
handler TestHandlerFunc
tokenHandler TestHandlerFunc
}{
{
name: "no credentials file",
plugin: &HTTP{
URL: u.String(),
},
handler: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
require.Empty(t, r.Header["Authorization"])
w.WriteHeader(http.StatusOK)
},
},
{
name: "success",
plugin: &HTTP{
URL: u.String() + "/write",
CredentialsFile: tmpFile.Name(),
},
tokenHandler: func(t *testing.T, w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
authHeader := fmt.Sprintf(`{"id_token":%q}`, token)
_, err = w.Write([]byte(authHeader))
require.NoError(t, err)
},
handler: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
require.Equal(t, []string{"Bearer " + token}, r.Header["Authorization"])
w.WriteHeader(http.StatusOK)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/write":
tt.handler(t, w, r)
case "/token":
tt.tokenHandler(t, w, r)
}
})
serializer := &influx.Serializer{}
require.NoError(t, serializer.Init())
tt.plugin.SetSerializer(serializer)
require.NoError(t, tt.plugin.Connect())
require.NoError(t, tt.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 userHeader := r.Header.Get("User-Agent"); userHeader != internal.ProductToken() {
w.WriteHeader(http.StatusInternalServerError)
t.Errorf("Not equal, expected: %q, actual: %q", internal.ProductToken(), userHeader)
return
}
w.WriteHeader(http.StatusOK)
})
client := &HTTP{
URL: u.String(),
Method: defaultMethod,
}
serializer := &influx.Serializer{}
require.NoError(t, serializer.Init())
client.SetSerializer(serializer)
err = client.Connect()
require.NoError(t, err)
err = client.Write([]telegraf.Metric{getMetric()})
require.NoError(t, err)
})
}
func TestBatchedUnbatched(t *testing.T) {
ts := httptest.NewServer(http.NotFoundHandler())
defer ts.Close()
u, err := url.Parse("http://" + ts.Listener.Addr().String())
require.NoError(t, err)
client := &HTTP{
URL: u.String(),
Method: defaultMethod,
}
influxSerializer := &influx.Serializer{}
require.NoError(t, influxSerializer.Init())
jsonSerializer := &json.Serializer{}
require.NoError(t, jsonSerializer.Init())
s := map[string]telegraf.Serializer{
"influx": influxSerializer,
"json": jsonSerializer,
}
for name, serializer := range s {
var requests int
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
requests++
w.WriteHeader(http.StatusOK)
})
t.Run(name, func(t *testing.T) {
for _, mode := range [...]bool{false, true} {
requests = 0
client.UseBatchFormat = mode
client.SetSerializer(serializer)
err = client.Connect()
require.NoError(t, err)
err = client.Write(getMetrics(3))
require.NoError(t, err)
if client.UseBatchFormat {
require.Equal(t, 1, requests, "batched")
} else {
require.Equal(t, 3, requests, "unbatched")
}
}
})
}
}
func TestAwsCredentials(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 *HTTP
tokenHandler TestHandlerFunc
handler TestHandlerFunc
}{
{
name: "simple credentials",
plugin: &HTTP{
URL: u.String(),
AwsService: "aps",
CredentialConfig: common_aws.CredentialConfig{
Region: "us-east-1",
AccessKey: "dummy",
SecretKey: "dummy",
},
},
handler: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
require.Contains(t, r.Header["Authorization"][0], "AWS4-HMAC-SHA256")
require.Contains(t, r.Header["Authorization"][0], "=dummy/")
require.Contains(t, r.Header["Authorization"][0], "/us-east-1/")
w.WriteHeader(http.StatusOK)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tt.handler(t, w, r)
})
serializer := &influx.Serializer{}
require.NoError(t, serializer.Init())
tt.plugin.SetSerializer(serializer)
err = tt.plugin.Connect()
require.NoError(t, err)
err = tt.plugin.Write([]telegraf.Metric{getMetric()})
require.NoError(t, err)
})
}
}

View file

@ -0,0 +1,112 @@
# A plugin that can transmit metrics over HTTP
[[outputs.http]]
## URL is the address to send metrics to
url = "http://127.0.0.1:8080/telegraf"
## Timeout for HTTP message
# timeout = "5s"
## HTTP method, one of: "POST" or "PUT" or "PATCH"
# method = "POST"
## HTTP Basic Auth credentials
# username = "username"
# password = "pa$$word"
## OAuth2 Client Credentials Grant
# client_id = "clientid"
# client_secret = "secret"
# token_url = "https://indentityprovider/oauth2/v1/token"
# audience = ""
# scopes = ["urn:opc:idm:__myscopes__"]
## Goole API Auth
# google_application_credentials = "/etc/telegraf/example_secret.json"
## HTTP Proxy support
# use_system_proxy = false
# http_proxy_url = ""
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
## Optional Cookie authentication
# cookie_auth_url = "https://localhost/authMe"
# cookie_auth_method = "POST"
# cookie_auth_username = "username"
# cookie_auth_password = "pa$$word"
# cookie_auth_headers = '{"Content-Type": "application/json", "X-MY-HEADER":"hello"}'
# cookie_auth_body = '{"username": "user", "password": "pa$$word", "authenticate": "me"}'
## cookie_auth_renewal not set or set to "0" will auth once and never renew the cookie
# cookie_auth_renewal = "5m"
## Data format to output.
## Each data format has it's own unique set of configuration options, read
## more about them here:
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md
# data_format = "influx"
## Use batch serialization format (default) instead of line based format.
## Batch format is more efficient and should be used unless line based
## format is really needed.
# use_batch_format = true
## HTTP Content-Encoding for write request body, can be set to "gzip" to
## compress body or "identity" to apply no encoding.
# content_encoding = "identity"
## MaxIdleConns controls the maximum number of idle (keep-alive)
## connections across all hosts. Zero means no limit.
# max_idle_conn = 0
## MaxIdleConnsPerHost, if non-zero, controls the maximum idle
## (keep-alive) connections to keep per-host. If zero,
## DefaultMaxIdleConnsPerHost is used(2).
# max_idle_conn_per_host = 2
## Idle (keep-alive) connection timeout.
## Maximum amount of time before idle connection is closed.
## Zero means no limit.
# idle_conn_timeout = 0
## Amazon Region
#region = "us-east-1"
## Amazon Credentials
## Amazon Credentials are not built unless the following aws_service
## setting is set to a non-empty string. It may need to match the name of
## the service output to as well
#aws_service = "execute-api"
## Credentials are loaded in the following order
## 1) Web identity provider credentials via STS if role_arn and web_identity_token_file are specified
## 2) Assumed credentials via STS if role_arn is specified
## 3) explicit credentials from 'access_key' and 'secret_key'
## 4) shared profile from 'profile'
## 5) environment variables
## 6) shared credentials file
## 7) EC2 Instance Profile
#access_key = ""
#secret_key = ""
#token = ""
#role_arn = ""
#web_identity_token_file = ""
#role_session_name = ""
#profile = ""
#shared_credential_file = ""
## Optional list of statuscodes (<200 or >300) upon which requests should not be retried
# non_retryable_statuscodes = [409, 413]
## NOTE: Due to the way TOML is parsed, tables must be at the END of the
## plugin definition, otherwise additional config options are read as part of
## the table
## Additional HTTP headers
# [outputs.http.headers]
# ## Should be set manually to "application/json" for json data_format
# Content-Type = "text/plain; charset=utf-8"