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
75
plugins/outputs/websocket/README.md
Normal file
75
plugins/outputs/websocket/README.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# Websocket Output Plugin
|
||||
|
||||
This plugin writes metrics to a WebSocket endpoint in one of the supported
|
||||
[data formats][data_formats].
|
||||
|
||||
⭐ Telegraf v1.19.0
|
||||
🏷️ applications, web
|
||||
💻 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 `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 WebSocket.
|
||||
[[outputs.websocket]]
|
||||
## URL is the address to send metrics to. Make sure ws or wss scheme is used.
|
||||
url = "ws://127.0.0.1:3000/telegraf"
|
||||
|
||||
## Timeouts (make sure read_timeout is larger than server ping interval or set to zero).
|
||||
# connect_timeout = "30s"
|
||||
# write_timeout = "30s"
|
||||
# read_timeout = "30s"
|
||||
|
||||
## Optionally turn on using text data frames (binary by default).
|
||||
# use_text_frames = false
|
||||
|
||||
## 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 SOCKS5 proxy to use
|
||||
# socks5_enabled = true
|
||||
# socks5_address = "127.0.0.1:1080"
|
||||
# socks5_username = "alice"
|
||||
# socks5_password = "pass123"
|
||||
|
||||
## Optional HTTP proxy to use
|
||||
# use_system_proxy = false
|
||||
# http_proxy_url = "http://localhost:8888"
|
||||
|
||||
## 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"
|
||||
|
||||
## 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 Upgrade headers
|
||||
# [outputs.websocket.headers]
|
||||
# Authorization = "Bearer <TOKEN>"
|
||||
```
|
43
plugins/outputs/websocket/sample.conf
Normal file
43
plugins/outputs/websocket/sample.conf
Normal file
|
@ -0,0 +1,43 @@
|
|||
# A plugin that can transmit metrics over WebSocket.
|
||||
[[outputs.websocket]]
|
||||
## URL is the address to send metrics to. Make sure ws or wss scheme is used.
|
||||
url = "ws://127.0.0.1:3000/telegraf"
|
||||
|
||||
## Timeouts (make sure read_timeout is larger than server ping interval or set to zero).
|
||||
# connect_timeout = "30s"
|
||||
# write_timeout = "30s"
|
||||
# read_timeout = "30s"
|
||||
|
||||
## Optionally turn on using text data frames (binary by default).
|
||||
# use_text_frames = false
|
||||
|
||||
## 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 SOCKS5 proxy to use
|
||||
# socks5_enabled = true
|
||||
# socks5_address = "127.0.0.1:1080"
|
||||
# socks5_username = "alice"
|
||||
# socks5_password = "pass123"
|
||||
|
||||
## Optional HTTP proxy to use
|
||||
# use_system_proxy = false
|
||||
# http_proxy_url = "http://localhost:8888"
|
||||
|
||||
## 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"
|
||||
|
||||
## 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 Upgrade headers
|
||||
# [outputs.websocket.headers]
|
||||
# Authorization = "Bearer <TOKEN>"
|
208
plugins/outputs/websocket/websocket.go
Normal file
208
plugins/outputs/websocket/websocket.go
Normal file
|
@ -0,0 +1,208 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package websocket
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
ws "github.com/gorilla/websocket"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/plugins/common/proxy"
|
||||
"github.com/influxdata/telegraf/plugins/common/tls"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
const (
|
||||
defaultConnectTimeout = 30 * time.Second
|
||||
defaultWriteTimeout = 30 * time.Second
|
||||
defaultReadTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// WebSocket can output to WebSocket endpoint.
|
||||
type WebSocket struct {
|
||||
URL string `toml:"url"`
|
||||
ConnectTimeout config.Duration `toml:"connect_timeout"`
|
||||
WriteTimeout config.Duration `toml:"write_timeout"`
|
||||
ReadTimeout config.Duration `toml:"read_timeout"`
|
||||
Headers map[string]*config.Secret `toml:"headers"`
|
||||
UseTextFrames bool `toml:"use_text_frames"`
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
proxy.HTTPProxy
|
||||
proxy.Socks5ProxyConfig
|
||||
tls.ClientConfig
|
||||
|
||||
conn *ws.Conn
|
||||
serializer telegraf.Serializer
|
||||
}
|
||||
|
||||
func (*WebSocket) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
// SetSerializer implements serializers.SerializerOutput.
|
||||
func (w *WebSocket) SetSerializer(serializer telegraf.Serializer) {
|
||||
w.serializer = serializer
|
||||
}
|
||||
|
||||
var errInvalidURL = errors.New("invalid websocket URL")
|
||||
|
||||
// Init the output plugin.
|
||||
func (w *WebSocket) Init() error {
|
||||
if parsedURL, err := url.Parse(w.URL); err != nil || (parsedURL.Scheme != "ws" && parsedURL.Scheme != "wss") {
|
||||
return fmt.Errorf("%w: %q", errInvalidURL, w.URL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect to the output endpoint.
|
||||
func (w *WebSocket) Connect() error {
|
||||
tlsCfg, err := w.ClientConfig.TLSConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating TLS config: %w", err)
|
||||
}
|
||||
|
||||
dialProxy, err := w.HTTPProxy.Proxy()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating proxy: %w", err)
|
||||
}
|
||||
|
||||
dialer := &ws.Dialer{
|
||||
Proxy: dialProxy,
|
||||
HandshakeTimeout: time.Duration(w.ConnectTimeout),
|
||||
TLSClientConfig: tlsCfg,
|
||||
}
|
||||
|
||||
if w.Socks5ProxyEnabled {
|
||||
netDialer, err := w.Socks5ProxyConfig.GetDialer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to socks5 proxy: %w", err)
|
||||
}
|
||||
dialer.NetDial = netDialer.Dial
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
for k, v := range w.Headers {
|
||||
secret, err := v.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting header secret %q failed: %w", k, err)
|
||||
}
|
||||
|
||||
headers.Set(k, secret.String())
|
||||
secret.Destroy()
|
||||
}
|
||||
|
||||
conn, resp, err := dialer.Dial(w.URL, headers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error dial: %w", err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
return fmt.Errorf("wrong status code while connecting to server: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
w.conn = conn
|
||||
go w.read(conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WebSocket) read(conn *ws.Conn) {
|
||||
defer func() { _ = conn.Close() }()
|
||||
if w.ReadTimeout > 0 {
|
||||
if err := conn.SetReadDeadline(time.Now().Add(time.Duration(w.ReadTimeout))); err != nil {
|
||||
w.Log.Errorf("error setting read deadline: %v", err)
|
||||
return
|
||||
}
|
||||
conn.SetPingHandler(func(string) error {
|
||||
err := conn.SetReadDeadline(time.Now().Add(time.Duration(w.ReadTimeout)))
|
||||
if err != nil {
|
||||
w.Log.Errorf("error setting read deadline: %v", err)
|
||||
return err
|
||||
}
|
||||
return conn.WriteControl(ws.PongMessage, nil, time.Now().Add(time.Duration(w.WriteTimeout)))
|
||||
})
|
||||
}
|
||||
for {
|
||||
// Need to read a connection (to properly process pings from a server).
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
// Websocket connection is not readable after first error, it's going to error state.
|
||||
// In the beginning of this goroutine we have defer section that closes such connection.
|
||||
// After that connection will be tried to reestablish on next Write.
|
||||
if ws.IsUnexpectedCloseError(err, ws.CloseGoingAway, ws.CloseAbnormalClosure) {
|
||||
w.Log.Errorf("error reading websocket connection: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if w.ReadTimeout > 0 {
|
||||
if err := conn.SetReadDeadline(time.Now().Add(time.Duration(w.ReadTimeout))); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes the given metrics to the destination. Not thread-safe.
|
||||
func (w *WebSocket) Write(metrics []telegraf.Metric) error {
|
||||
if w.conn == nil {
|
||||
// Previous write failed with error and ws conn was closed.
|
||||
if err := w.Connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
messageData, err := w.serializer.SerializeBatch(metrics)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.WriteTimeout > 0 {
|
||||
if err := w.conn.SetWriteDeadline(time.Now().Add(time.Duration(w.WriteTimeout))); err != nil {
|
||||
return fmt.Errorf("error setting write deadline: %w", err)
|
||||
}
|
||||
}
|
||||
messageType := ws.BinaryMessage
|
||||
if w.UseTextFrames {
|
||||
messageType = ws.TextMessage
|
||||
}
|
||||
err = w.conn.WriteMessage(messageType, messageData)
|
||||
if err != nil {
|
||||
_ = w.conn.Close()
|
||||
w.conn = nil
|
||||
return fmt.Errorf("error writing to connection: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the connection. Noop if already closed.
|
||||
func (w *WebSocket) Close() error {
|
||||
if w.conn == nil {
|
||||
return nil
|
||||
}
|
||||
err := w.conn.Close()
|
||||
w.conn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func newWebSocket() *WebSocket {
|
||||
return &WebSocket{
|
||||
ConnectTimeout: config.Duration(defaultConnectTimeout),
|
||||
WriteTimeout: config.Duration(defaultWriteTimeout),
|
||||
ReadTimeout: config.Duration(defaultReadTimeout),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("websocket", func() telegraf.Output {
|
||||
return newWebSocket()
|
||||
})
|
||||
}
|
223
plugins/outputs/websocket/websocket_test.go
Normal file
223
plugins/outputs/websocket/websocket_test.go
Normal file
|
@ -0,0 +1,223 @@
|
|||
package websocket
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
// testSerializer serializes to a number of metrics to simplify tests here.
|
||||
type testSerializer struct{}
|
||||
|
||||
func newTestSerializer() *testSerializer {
|
||||
return &testSerializer{}
|
||||
}
|
||||
|
||||
func (testSerializer) Serialize(_ telegraf.Metric) ([]byte, error) {
|
||||
return []byte("1"), nil
|
||||
}
|
||||
|
||||
func (testSerializer) SerializeBatch(metrics []telegraf.Metric) ([]byte, error) {
|
||||
return []byte(strconv.Itoa(len(metrics))), nil
|
||||
}
|
||||
|
||||
type testServer struct {
|
||||
*httptest.Server
|
||||
t *testing.T
|
||||
messages chan []byte
|
||||
upgradeDelay time.Duration
|
||||
expectTextFrames bool
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T, messages chan []byte, tls bool) *testServer {
|
||||
s := &testServer{}
|
||||
s.t = t
|
||||
if tls {
|
||||
s.Server = httptest.NewTLSServer(s)
|
||||
} else {
|
||||
s.Server = httptest.NewServer(s)
|
||||
}
|
||||
s.URL = makeWsProto(s.Server.URL)
|
||||
s.messages = messages
|
||||
return s
|
||||
}
|
||||
|
||||
func makeWsProto(s string) string {
|
||||
return "ws" + strings.TrimPrefix(s, "http")
|
||||
}
|
||||
|
||||
const (
|
||||
testHeaderName = "X-Telegraf-Test"
|
||||
testHeaderValue = "1"
|
||||
)
|
||||
|
||||
var testUpgrader = ws.Upgrader{}
|
||||
|
||||
func (s *testServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get(testHeaderName) != testHeaderValue {
|
||||
s.t.Fatalf("expected test header found in request, got: %#v", r.Header)
|
||||
}
|
||||
if s.upgradeDelay > 0 {
|
||||
// Emulate long handshake.
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-time.After(s.upgradeDelay):
|
||||
}
|
||||
}
|
||||
conn, err := testUpgrader.Upgrade(w, r, http.Header{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
for {
|
||||
messageType, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if s.expectTextFrames && messageType != ws.TextMessage {
|
||||
s.t.Fatalf("unexpected frame type: %d", messageType)
|
||||
}
|
||||
select {
|
||||
case s.messages <- data:
|
||||
case <-time.After(5 * time.Second):
|
||||
s.t.Fatal("timeout writing to messages channel, make sure there are readers")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initWebSocket(s *testServer) *WebSocket {
|
||||
w := newWebSocket()
|
||||
w.Log = testutil.Logger{}
|
||||
w.URL = s.URL
|
||||
headerSecret := config.NewSecret([]byte(testHeaderValue))
|
||||
w.Headers = map[string]*config.Secret{testHeaderName: &headerSecret}
|
||||
w.SetSerializer(newTestSerializer())
|
||||
return w
|
||||
}
|
||||
|
||||
func connect(t *testing.T, w *WebSocket) {
|
||||
err := w.Connect()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWebSocket_NoURL(t *testing.T) {
|
||||
w := newWebSocket()
|
||||
err := w.Init()
|
||||
require.ErrorIs(t, err, errInvalidURL)
|
||||
}
|
||||
|
||||
func TestWebSocket_Connect_Timeout(t *testing.T) {
|
||||
s := newTestServer(t, nil, false)
|
||||
s.upgradeDelay = time.Second
|
||||
defer s.Close()
|
||||
w := initWebSocket(s)
|
||||
w.ConnectTimeout = config.Duration(10 * time.Millisecond)
|
||||
err := w.Connect()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWebSocket_Connect_OK(t *testing.T) {
|
||||
s := newTestServer(t, nil, false)
|
||||
defer s.Close()
|
||||
w := initWebSocket(s)
|
||||
connect(t, w)
|
||||
}
|
||||
|
||||
func TestWebSocket_ConnectTLS_OK(t *testing.T) {
|
||||
s := newTestServer(t, nil, true)
|
||||
defer s.Close()
|
||||
w := initWebSocket(s)
|
||||
w.ClientConfig.InsecureSkipVerify = true
|
||||
connect(t, w)
|
||||
}
|
||||
|
||||
func TestWebSocket_Write_OK(t *testing.T) {
|
||||
messages := make(chan []byte, 1)
|
||||
|
||||
s := newTestServer(t, messages, false)
|
||||
defer s.Close()
|
||||
|
||||
w := initWebSocket(s)
|
||||
connect(t, w)
|
||||
|
||||
metrics := []telegraf.Metric{
|
||||
testutil.TestMetric(0.4, "test"),
|
||||
testutil.TestMetric(0.5, "test"),
|
||||
}
|
||||
err := w.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case data := <-messages:
|
||||
require.Equal(t, []byte("2"), data)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timeout receiving data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocket_Write_Error(t *testing.T) {
|
||||
s := newTestServer(t, nil, false)
|
||||
defer s.Close()
|
||||
|
||||
w := initWebSocket(s)
|
||||
connect(t, w)
|
||||
|
||||
require.NoError(t, w.conn.Close())
|
||||
|
||||
metrics := []telegraf.Metric{testutil.TestMetric(0.4, "test")}
|
||||
err := w.Write(metrics)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, w.conn)
|
||||
}
|
||||
|
||||
func TestWebSocket_Write_Reconnect(t *testing.T) {
|
||||
messages := make(chan []byte, 1)
|
||||
s := newTestServer(t, messages, false)
|
||||
s.expectTextFrames = true // Also use text frames in this test.
|
||||
defer s.Close()
|
||||
|
||||
w := initWebSocket(s)
|
||||
w.UseTextFrames = true
|
||||
connect(t, w)
|
||||
|
||||
metrics := []telegraf.Metric{testutil.TestMetric(0.4, "test")}
|
||||
|
||||
require.NoError(t, w.conn.Close())
|
||||
|
||||
err := w.Write(metrics)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, w.conn)
|
||||
|
||||
err = w.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case data := <-messages:
|
||||
require.Equal(t, []byte("1"), data)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timeout receiving data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocket_Close(t *testing.T) {
|
||||
s := newTestServer(t, nil, false)
|
||||
defer s.Close()
|
||||
|
||||
w := initWebSocket(s)
|
||||
connect(t, w)
|
||||
require.NoError(t, w.Close())
|
||||
// Check no error on second close.
|
||||
require.NoError(t, w.Close())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue