feat(inputs.http_listener_v2): Add unix socket mode (#15764)

This commit is contained in:
bazko1 2024-09-04 18:37:06 +02:00 committed by GitHub
parent b00de66a3f
commit 0b4f77dc1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 128 additions and 15 deletions

View File

@ -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"]

View File

@ -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

View File

@ -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)

View File

@ -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"]