Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion internal/collector/otel_collector_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"log/slog"
"net"
"net/url"
"os"
"strings"
"sync"
Expand Down Expand Up @@ -221,7 +222,7 @@ func (oc *Collector) processReceivers(ctx context.Context, receivers map[string]
}
}

//nolint:revive // cognitive complexity is 13
//nolint:revive,cyclop // cognitive complexity is 13
func (oc *Collector) bootup(ctx context.Context) error {
errChan := make(chan error)

Expand All @@ -231,6 +232,10 @@ func (oc *Collector) bootup(ctx context.Context) error {
return
}

if oc.config.IsCommandServerProxyConfigured() {
oc.setProxyIfNeeded(ctx)
}

appErr := oc.service.Run(ctx)
if appErr != nil {
errChan <- appErr
Expand Down Expand Up @@ -394,6 +399,10 @@ func (oc *Collector) restartCollector(ctx context.Context) {
}
oc.service = oTelCollector

if oc.config.IsCommandServerProxyConfigured() {
oc.setProxyIfNeeded(ctx)
}

var runCtx context.Context
runCtx, oc.cancel = context.WithCancel(ctx)

Expand All @@ -409,6 +418,14 @@ func (oc *Collector) restartCollector(ctx context.Context) {
}
}

func (oc *Collector) setProxyIfNeeded(ctx context.Context) {
if oc.config.Collector.Exporters.OtlpExporters != nil ||
oc.config.Collector.Exporters.PrometheusExporter != nil {
// Set proxy env vars for OTLP exporter if proxy is configured.
oc.setExporterProxyEnvVars(ctx)
}
}

func (oc *Collector) checkForNewReceivers(ctx context.Context, nginxConfigContext *model.NginxConfigContext) bool {
nginxReceiverFound, reloadCollector := oc.updateExistingNginxPlusReceiver(nginxConfigContext)

Expand Down Expand Up @@ -742,3 +759,59 @@ func escapeString(input string) string {

return output
}

func (oc *Collector) setExporterProxyEnvVars(ctx context.Context) {
proxy := oc.config.Command.Server.Proxy
proxyURL := proxy.URL
parsedProxyURL, err := url.Parse(proxyURL)
if err != nil {
slog.ErrorContext(ctx, "Malformed proxy URL, unable to configure proxy for OTLP exporter",
"url", proxyURL, "error", err)

return
}

if parsedProxyURL.Scheme == "https" {
slog.ErrorContext(ctx, "HTTPS protocol not supported by OTLP exporter, unable to configure proxy for "+
"OTLP exporter", "url", proxyURL)
}

auth := ""
if proxy.AuthMethod != "" && strings.TrimSpace(proxy.AuthMethod) != "" {
auth = strings.TrimSpace(proxy.AuthMethod)
}

// Use the standalone setProxyWithBasicAuth function
if auth == "" {
setProxyEnvs(ctx, proxyURL, "Setting Proxy from command.Proxy (no auth)")
return
}
authLower := strings.ToLower(auth)
if authLower == "basic" {
setProxyWithBasicAuth(ctx, proxy, parsedProxyURL)
} else {
slog.ErrorContext(ctx, "Unknown auth type for proxy, unable to configure proxy for OTLP exporter",
"auth", auth, "url", proxyURL)
}
}

// setProxyEnvs sets the HTTP_PROXY and HTTPS_PROXY environment variables and logs the action.
func setProxyEnvs(ctx context.Context, proxyEnvURL, msg string) {
slog.DebugContext(ctx, msg, "url", proxyEnvURL)
if setenvErr := os.Setenv("HTTPS_PROXY", proxyEnvURL); setenvErr != nil {
slog.ErrorContext(ctx, "Failed to set OTLP exporter proxy environment variables", "error", setenvErr)
}
}

// setProxyWithBasicAuth sets the proxy environment variables with basic auth credentials.
func setProxyWithBasicAuth(ctx context.Context, proxy *config.Proxy, parsedProxyURL *url.URL) {
username := proxy.Username
password := proxy.Password
if username == "" || password == "" {
slog.ErrorContext(ctx, "Unable to configure OTLP exporter proxy, username or password missing for basic auth")
return
}
parsedProxyURL.User = url.UserPassword(username, password)
proxyURL := parsedProxyURL.String()
setProxyEnvs(ctx, proxyURL, "Setting Proxy with basic auth")
}
130 changes: 129 additions & 1 deletion internal/collector/otel_collector_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"context"
"errors"
"net"
"net/url"
"os"
"path/filepath"
"testing"

Expand Down Expand Up @@ -246,7 +248,11 @@ func TestCollector_ProcessNginxConfigUpdateTopic(t *testing.T) {

conf := types.OTelConfig(t)

conf.Command = nil
conf.Command = &config.Command{
Server: &config.ServerConfig{
Proxy: &config.Proxy{},
},
}

conf.Collector.Log.Path = ""
conf.Collector.Receivers.HostMetrics = nil
Expand Down Expand Up @@ -782,6 +788,128 @@ func TestCollector_updateNginxAppProtectTcplogReceivers(t *testing.T) {
})
}

func Test_setProxyEnvs(t *testing.T) {
ctx := context.Background()
proxyURL := "http://localhost:8080"
msg := "Setting test proxy"

// Unset first to ensure clean state
_ = os.Unsetenv("HTTPS_PROXY")

setProxyEnvs(ctx, proxyURL, msg)

httpProxy := os.Getenv("HTTPS_PROXY")
assert.Equal(t, proxyURL, httpProxy)
}

func Test_setProxyWithBasicAuth(t *testing.T) {
ctx := context.Background()
u, _ := url.Parse("http://localhost:8080")
proxy := &config.Proxy{
URL: "http://localhost:8080",
Username: "user",
Password: "pass",
}

// Unset first to ensure clean state
_ = os.Unsetenv("HTTPS_PROXY")

setProxyWithBasicAuth(ctx, proxy, u)

proxyURL := u.String()
httpProxy := os.Getenv("HTTPS_PROXY")
assert.Equal(t, proxyURL, httpProxy)

// Test missing username/password
proxyMissing := &config.Proxy{URL: "http://localhost:8080"}
setProxyWithBasicAuth(ctx, proxyMissing, u) // Should not panic
}

//nolint:contextcheck // Can not update the "OTelConfig" function definition
func TestSetExporterProxyEnvVars(t *testing.T) {
ctx := context.Background()
logBuf := &bytes.Buffer{}
stub.StubLoggerWith(logBuf)

tests := []struct {
name string
proxy *config.Proxy
expectedLog string
setEnv bool
}{
{
name: "Test 1: No proxy config",
proxy: nil,
expectedLog: "Proxy configuration is not setup. Unable to configure proxy for OTLP exporter",
setEnv: false,
},
{
name: "Test 2: Malformed proxy URL",
proxy: &config.Proxy{URL: "://bad_url"},
expectedLog: "Malformed proxy URL, unable to configure proxy for OTLP exporter",
setEnv: false,
},
{
name: "Test 3: No auth, valid URL",
proxy: &config.Proxy{URL: "http://proxy.example.com:8080"},
expectedLog: "Setting Proxy from command.Proxy (no auth)",
setEnv: true,
},
{
name: "Basic auth, valid URL",
proxy: &config.Proxy{
URL: "http://proxy.example.com:8080",
AuthMethod: "basic",
Username: "user",
Password: "pass",
},
expectedLog: "Setting Proxy with basic auth",
setEnv: true,
},
{
name: "Unknown auth method",
proxy: &config.Proxy{URL: "http://proxy.example.com:8080", AuthMethod: "digest"},
expectedLog: "Unknown auth type for proxy, unable to configure proxy for OTLP exporter",
setEnv: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logBuf.Reset()

_ = os.Unsetenv("HTTPS_PROXY")

tmpDir := t.TempDir()
cfg := types.OTelConfig(t)
cfg.Collector.Log.Path = filepath.Join(tmpDir, "otel-collector-test.log")
cfg.Command.Server.Proxy = tt.proxy

// If the proxy is nil, the production code would never call the setter functions.
// added this check to prevent the panic error in UT.
if cfg.Command.Server.Proxy == nil {
// For the nil proxy case, we expect nothing to happen.
assert.Empty(t, os.Getenv("HTTPS_PROXY"))

return
}

collector, err := NewCollector(cfg)
require.NoError(t, err)

collector.setExporterProxyEnvVars(ctx)

helpers.ValidateLog(t, tt.expectedLog, logBuf)

if tt.setEnv {
assert.NotEmpty(t, os.Getenv("HTTPS_PROXY"))
} else {
assert.Empty(t, os.Getenv("HTTPS_PROXY"))
}
})
}
}

func TestCollector_findAvailableSyslogServers(t *testing.T) {
ctx := context.Background()
conf := types.OTelConfig(t)
Expand Down
110 changes: 107 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,65 @@ func registerCommandFlags(fs *flag.FlagSet) {
DefCommandTLServerNameKey,
"Specifies the name of the server sent in the TLS configuration.",
)
fs.Duration(
CommandServerProxyTimeoutKey,
DefCommandServerProxyTimeoutKey,
"The explicit forward proxy HTTP Timeout, value in seconds")
fs.String(
CommandServerProxyURLKey,
DefCommandServerProxyURlKey,
"The Proxy URL to use for explicit forward proxy.",
)
fs.String(
CommandServerProxyNoProxyKey,
DefCommandServerProxyNoProxyKey,
"The No-Proxy URL to use for explicit forward proxy.",
)
fs.String(
CommandServerProxyAuthMethodKey,
DefCommandServerProxyAuthMethodKey,
"The Authentication method used for explicit forward proxy.",
)
fs.String(
CommandServerProxyUsernameKey,
DefCommandServerProxyUsernameKey,
"The Username used for basic authentication for explicit forward proxy.",
)
fs.String(
CommandServerProxyPasswordKey,
DefCommandServerProxyPasswordKey,
"The Password used for basic authentication for explicit forward proxy.",
)
fs.String(
CommandServerProxyTokenKey,
DefCommandServerProxyTokenKey,
"The bearer token used for authentication for explicit forward proxy.",
)
fs.String(
CommandServerProxyTLSCertKey,
DefCommandServerProxyTLSCertKey,
"The path to the certificate file to use for TLS communication with the command server.",
)
fs.String(
CommandServerProxyTLSKeyKey,
DefCommandServerProxyTLSKeyKey,
"The path to the certificate key file to use for TLS communication with the command server.",
)
fs.String(
CommandServerProxyTLSCaKey,
DefCommandServerProxyTLSCaKey,
"The path to CA certificate file to use for TLS communication with the command server.",
)
fs.Bool(
CommandServerProxyTLSSkipVerifyKey,
DefCommandServerProxyTLSSkipVerifyKey,
"Testing only. Skip verify controls client verification of a server's certificate chain and host name.",
)
fs.String(
CommandServerProxyTLSServerNameKey,
DefCommandServerProxyTLServerNameKey,
"Specifies the name of the server sent in the TLS configuration.",
)
}

func registerAuxiliaryCommandFlags(fs *flag.FlagSet) {
Expand Down Expand Up @@ -1235,9 +1294,10 @@ func resolveCommand() *Command {

command := &Command{
Server: &ServerConfig{
Host: viperInstance.GetString(CommandServerHostKey),
Port: viperInstance.GetInt(CommandServerPortKey),
Type: serverType,
Host: viperInstance.GetString(CommandServerHostKey),
Port: viperInstance.GetInt(CommandServerPortKey),
Type: serverType,
Proxy: resolveProxy(),
},
}

Expand Down Expand Up @@ -1365,3 +1425,47 @@ func resolveMapStructure(key string, object any) error {

return nil
}

func resolveProxy() *Proxy {
proxy := &Proxy{
Timeout: viperInstance.GetDuration(CommandServerProxyTimeoutKey),
URL: viperInstance.GetString(CommandServerProxyURLKey),
NoProxy: viperInstance.GetString(CommandServerProxyNoProxyKey),
Username: viperInstance.GetString(CommandServerProxyUsernameKey),
Password: viperInstance.GetString(CommandServerProxyPasswordKey),
Token: viperInstance.GetString(CommandServerProxyTokenKey),
AuthMethod: viperInstance.GetString(CommandServerProxyAuthMethodKey),
}

if areCommandServerProxyTLSSettingsSet() {
proxy.TLS = &TLSConfig{
Cert: viperInstance.GetString(CommandServerProxyTLSCertKey),
Key: viperInstance.GetString(CommandServerProxyTLSKeyKey),
Ca: viperInstance.GetString(CommandServerProxyTLSCaKey),
SkipVerify: viperInstance.GetBool(CommandServerProxyTLSSkipVerifyKey),
ServerName: viperInstance.GetString(CommandServerProxyTLSServerNameKey),
}
}

// If all fields are zero/nil/empty, return nil
if proxy.TLS == nil &&
proxy.Timeout == 0 &&
proxy.URL == "" &&
proxy.NoProxy == "" &&
proxy.AuthMethod == "" &&
proxy.Username == "" &&
proxy.Password == "" &&
proxy.Token == "" {
return nil
}

return proxy
}

func areCommandServerProxyTLSSettingsSet() bool {
return viperInstance.IsSet(CommandServerProxyTLSCertKey) ||
viperInstance.IsSet(CommandServerProxyTLSKeyKey) ||
viperInstance.IsSet(CommandServerProxyTLSCaKey) ||
viperInstance.IsSet(CommandServerProxyTLSSkipVerifyKey) ||
viperInstance.IsSet(CommandServerProxyTLSServerNameKey)
}
Loading