feat(inputs.http_listener_v2): Add unix socket mode (#15764)
This commit is contained in:
parent
b00de66a3f
commit
0b4f77dc1d
|
|
@ -36,8 +36,19 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||||
```toml @sample.conf
|
```toml @sample.conf
|
||||||
# Generic HTTP write listener
|
# Generic HTTP write listener
|
||||||
[[inputs.http_listener_v2]]
|
[[inputs.http_listener_v2]]
|
||||||
## Address and port to host HTTP listener on
|
## Address to host HTTP listener on
|
||||||
service_address = ":8080"
|
## can be prefixed by protocol tcp, or unix if not provided defaults to tcp
|
||||||
|
## if unix network type provided it should be followed by absolute path for unix socket
|
||||||
|
service_address = "tcp://:8080"
|
||||||
|
## service_address = "tcp://:8443"
|
||||||
|
## service_address = "unix:///tmp/telegraf.sock"
|
||||||
|
|
||||||
|
## Permission for unix sockets (only available for unix sockets)
|
||||||
|
## This setting may not be respected by some platforms. To safely restrict
|
||||||
|
## permissions it is recommended to place the socket into a previously
|
||||||
|
## created directory with the desired permissions.
|
||||||
|
## ex: socket_mode = "777"
|
||||||
|
# socket_mode = ""
|
||||||
|
|
||||||
## Paths to listen to.
|
## Paths to listen to.
|
||||||
# paths = ["/telegraf"]
|
# paths = ["/telegraf"]
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,16 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -47,6 +53,7 @@ type TimeFunc func() time.Time
|
||||||
// HTTPListenerV2 is an input plugin that collects external metrics sent via HTTP
|
// HTTPListenerV2 is an input plugin that collects external metrics sent via HTTP
|
||||||
type HTTPListenerV2 struct {
|
type HTTPListenerV2 struct {
|
||||||
ServiceAddress string `toml:"service_address"`
|
ServiceAddress string `toml:"service_address"`
|
||||||
|
SocketMode string `toml:"socket_mode"`
|
||||||
Path string `toml:"path" deprecated:"1.20.0;1.35.0;use 'paths' instead"`
|
Path string `toml:"path" deprecated:"1.20.0;1.35.0;use 'paths' instead"`
|
||||||
Paths []string `toml:"paths"`
|
Paths []string `toml:"paths"`
|
||||||
PathTag bool `toml:"path_tag"`
|
PathTag bool `toml:"path_tag"`
|
||||||
|
|
@ -56,7 +63,7 @@ type HTTPListenerV2 struct {
|
||||||
ReadTimeout config.Duration `toml:"read_timeout"`
|
ReadTimeout config.Duration `toml:"read_timeout"`
|
||||||
WriteTimeout config.Duration `toml:"write_timeout"`
|
WriteTimeout config.Duration `toml:"write_timeout"`
|
||||||
MaxBodySize config.Size `toml:"max_body_size"`
|
MaxBodySize config.Size `toml:"max_body_size"`
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port" deprecated:"1.32.0;1.35.0;use 'service_address' instead"`
|
||||||
SuccessCode int `toml:"http_success_code"`
|
SuccessCode int `toml:"http_success_code"`
|
||||||
BasicUsername string `toml:"basic_username"`
|
BasicUsername string `toml:"basic_username"`
|
||||||
BasicPassword string `toml:"basic_password"`
|
BasicPassword string `toml:"basic_password"`
|
||||||
|
|
@ -72,6 +79,7 @@ type HTTPListenerV2 struct {
|
||||||
close chan struct{}
|
close chan struct{}
|
||||||
|
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
|
url *url.URL
|
||||||
|
|
||||||
telegraf.Parser
|
telegraf.Parser
|
||||||
acc telegraf.Accumulator
|
acc telegraf.Accumulator
|
||||||
|
|
@ -91,6 +99,49 @@ func (h *HTTPListenerV2) SetParser(parser telegraf.Parser) {
|
||||||
|
|
||||||
// Start starts the http listener service.
|
// Start starts the http listener service.
|
||||||
func (h *HTTPListenerV2) Start(acc telegraf.Accumulator) error {
|
func (h *HTTPListenerV2) Start(acc telegraf.Accumulator) error {
|
||||||
|
u := h.url
|
||||||
|
address := u.Host
|
||||||
|
switch u.Scheme {
|
||||||
|
case "tcp":
|
||||||
|
case "unix":
|
||||||
|
path := filepath.FromSlash(u.Path)
|
||||||
|
if runtime.GOOS == "windows" && strings.Contains(path, ":") {
|
||||||
|
path = strings.TrimPrefix(path, `\`)
|
||||||
|
}
|
||||||
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("removing socket failed: %w", err)
|
||||||
|
}
|
||||||
|
address = path
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown protocol %q", u.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener net.Listener
|
||||||
|
var err error
|
||||||
|
if h.tlsConf != nil {
|
||||||
|
listener, err = tls.Listen(u.Scheme, address, h.tlsConf)
|
||||||
|
} else {
|
||||||
|
listener, err = net.Listen(u.Scheme, address)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.listener = listener
|
||||||
|
|
||||||
|
if u.Scheme == "unix" && h.SocketMode != "" {
|
||||||
|
// Set permissions on socket
|
||||||
|
// Convert from octal in string to int
|
||||||
|
i, err := strconv.ParseUint(h.SocketMode, 8, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("converting socket mode failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
perm := os.FileMode(uint32(i))
|
||||||
|
if err := os.Chmod(address, perm); err != nil {
|
||||||
|
return fmt.Errorf("changing socket permissions failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if h.MaxBodySize == 0 {
|
if h.MaxBodySize == 0 {
|
||||||
h.MaxBodySize = config.Size(defaultMaxBodySize)
|
h.MaxBodySize = config.Size(defaultMaxBodySize)
|
||||||
}
|
}
|
||||||
|
|
@ -151,18 +202,18 @@ func (h *HTTPListenerV2) Init() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var listener net.Listener
|
protoRegex := regexp.MustCompile(`\w://`)
|
||||||
if tlsConf != nil {
|
if !protoRegex.MatchString(h.ServiceAddress) {
|
||||||
listener, err = tls.Listen("tcp", h.ServiceAddress, tlsConf)
|
h.ServiceAddress = "tcp://" + h.ServiceAddress
|
||||||
} else {
|
|
||||||
listener, err = net.Listen("tcp", h.ServiceAddress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(h.ServiceAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("parsing address failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.url = u
|
||||||
h.tlsConf = tlsConf
|
h.tlsConf = tlsConf
|
||||||
h.listener = listener
|
|
||||||
h.Port = listener.Addr().(*net.TCPAddr).Port
|
|
||||||
|
|
||||||
if h.SuccessCode == 0 {
|
if h.SuccessCode == 0 {
|
||||||
h.SuccessCode = http.StatusNoContent
|
h.SuccessCode = http.StatusNoContent
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,17 @@ package http_listener_v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -109,9 +112,13 @@ func getHTTPSClient() *http.Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
func createURL(listener *HTTPListenerV2, scheme string, path string, rawquery string) string {
|
func createURL(listener *HTTPListenerV2, scheme string, path string, rawquery string) string {
|
||||||
|
var port int
|
||||||
|
if strings.HasPrefix(listener.ServiceAddress, "tcp://") {
|
||||||
|
port = listener.listener.Addr().(*net.TCPAddr).Port
|
||||||
|
}
|
||||||
u := url.URL{
|
u := url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: "localhost:" + strconv.Itoa(listener.Port),
|
Host: "localhost:" + strconv.Itoa(port),
|
||||||
Path: path,
|
Path: path,
|
||||||
RawQuery: rawquery,
|
RawQuery: rawquery,
|
||||||
}
|
}
|
||||||
|
|
@ -134,7 +141,9 @@ func TestInvalidListenerConfig(t *testing.T) {
|
||||||
close: make(chan struct{}),
|
close: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
require.Error(t, listener.Init())
|
require.NoError(t, listener.Init())
|
||||||
|
acc := &testutil.Accumulator{}
|
||||||
|
require.Error(t, listener.Start(acc))
|
||||||
|
|
||||||
// Stop is called when any ServiceInput fails to start; it must succeed regardless of state
|
// Stop is called when any ServiceInput fails to start; it must succeed regardless of state
|
||||||
listener.Stop()
|
listener.Stop()
|
||||||
|
|
@ -724,6 +733,37 @@ func TestServerHeaders(t *testing.T) {
|
||||||
require.Equal(t, "value", resp.Header.Get("key"))
|
require.Equal(t, "value", resp.Header.Get("key"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnixSocket(t *testing.T) {
|
||||||
|
listener, err := newTestHTTPListenerV2()
|
||||||
|
require.NoError(t, err)
|
||||||
|
file, err := os.CreateTemp("", "*.socket")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, file.Close())
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
socketName := file.Name()
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
listener.ServiceAddress = "unix:///" + socketName
|
||||||
|
} else {
|
||||||
|
listener.ServiceAddress = "unix://" + socketName
|
||||||
|
}
|
||||||
|
listener.SocketMode = "777"
|
||||||
|
acc := &testutil.Accumulator{}
|
||||||
|
require.NoError(t, listener.Init())
|
||||||
|
require.NoError(t, listener.Start(acc))
|
||||||
|
defer listener.Stop()
|
||||||
|
httpc := http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return net.Dial("unix", socketName)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := httpc.Post(createURL(listener, "http", "/write", "db=mydb"), "", bytes.NewBufferString(testMsg))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
require.EqualValues(t, 204, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
func mustReadHugeMetric() []byte {
|
func mustReadHugeMetric() []byte {
|
||||||
filePath := "testdata/huge_metric"
|
filePath := "testdata/huge_metric"
|
||||||
data, err := os.ReadFile(filePath)
|
data, err := os.ReadFile(filePath)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,18 @@
|
||||||
# Generic HTTP write listener
|
# Generic HTTP write listener
|
||||||
[[inputs.http_listener_v2]]
|
[[inputs.http_listener_v2]]
|
||||||
## Address and port to host HTTP listener on
|
## Address to host HTTP listener on
|
||||||
service_address = ":8080"
|
## can be prefixed by protocol tcp, or unix if not provided defaults to tcp
|
||||||
|
## if unix network type provided it should be followed by absolute path for unix socket
|
||||||
|
service_address = "tcp://:8080"
|
||||||
|
## service_address = "tcp://:8443"
|
||||||
|
## service_address = "unix:///tmp/telegraf.sock"
|
||||||
|
|
||||||
|
## Permission for unix sockets (only available for unix sockets)
|
||||||
|
## This setting may not be respected by some platforms. To safely restrict
|
||||||
|
## permissions it is recommended to place the socket into a previously
|
||||||
|
## created directory with the desired permissions.
|
||||||
|
## ex: socket_mode = "777"
|
||||||
|
# socket_mode = ""
|
||||||
|
|
||||||
## Paths to listen to.
|
## Paths to listen to.
|
||||||
# paths = ["/telegraf"]
|
# paths = ["/telegraf"]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue