Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions src/core/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,9 @@ func getNginx() Nginx {
NginxClientVersion: Viper.GetInt(NginxClientVersion),
ConfigReloadMonitoringPeriod: Viper.GetDuration(NginxConfigReloadMonitoringPeriod),
TreatWarningsAsErrors: Viper.GetBool(NginxTreatWarningsAsErrors),
ApiTls: TLSConfig{
Ca: Viper.GetString(NginxApiTlsCa),
API: &NginxAPI{
URL: Viper.GetString(NginxApiURLKey),
TLS: TLSConfig{Ca: Viper.GetString(NginxApiTlsCa)},
},
}
}
Expand Down
15 changes: 12 additions & 3 deletions src/core/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ var (
NginxClientVersion: 7, // NGINX Plus R25+
ConfigReloadMonitoringPeriod: 10 * time.Second,
TreatWarningsAsErrors: false,
ApiTls: TLSConfig{
Ca: "",
API: &NginxAPI{
URL: "",
TLS: TLSConfig{
Ca: "",
},
},
},
ConfigDirs: "/etc/nginx:/usr/local/etc/nginx:/usr/share/nginx/modules:/etc/nms",
Expand Down Expand Up @@ -179,6 +182,7 @@ const (
NginxConfigReloadMonitoringPeriod = NginxKey + agent_config.KeyDelimiter + "config_reload_monitoring_period"
NginxTreatWarningsAsErrors = NginxKey + agent_config.KeyDelimiter + "treat_warnings_as_errors"
NginxApiTlsCa = NginxKey + agent_config.KeyDelimiter + "api_tls_ca"
NginxApiURLKey = NginxKey + agent_config.KeyDelimiter + "api_url"

// viper keys used in config
DataplaneKey = "dataplane"
Expand Down Expand Up @@ -325,10 +329,15 @@ var (
Usage: "On nginx -t, treat warnings as failures on configuration application.",
DefaultValue: Defaults.Nginx.TreatWarningsAsErrors,
},
&StringFlag{
Name: NginxApiURLKey,
Usage: "The NGINX Plus API URL.",
DefaultValue: Defaults.Nginx.API.URL,
},
&StringFlag{
Name: NginxApiTlsCa,
Usage: "The NGINX Plus CA certificate file location needed to call the NGINX Plus API if SSL is enabled.",
DefaultValue: Defaults.Nginx.ApiTls.Ca,
DefaultValue: Defaults.Nginx.API.TLS.Ca,
},
// Metrics
&DurationFlag{
Expand Down
7 changes: 6 additions & 1 deletion src/core/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,12 @@ type Nginx struct {
NginxClientVersion int `mapstructure:"client_version" yaml:"-"`
ConfigReloadMonitoringPeriod time.Duration `mapstructure:"config_reload_monitoring_period" yaml:"-"`
TreatWarningsAsErrors bool `mapstructure:"treat_warnings_as_errors" yaml:"-"`
ApiTls TLSConfig `mapstructure:"api_tls" yaml:"-"`
API *NginxAPI `mapstructure:"api"`
}

type NginxAPI struct {
URL string `mapstructure:"url"`
TLS TLSConfig `mapstructure:"tls"`
}

type Dataplane struct {
Expand Down
4 changes: 2 additions & 2 deletions src/core/metrics/sources/nginx_plus.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ func NewNginxPlus(baseDimensions *metrics.CommonDim, nginxNamespace, plusNamespa

client := http.DefaultClient

if conf.Nginx.ApiTls.Ca != "" && conf.IsFileAllowed(conf.Nginx.ApiTls.Ca) {
data, err := os.ReadFile(conf.Nginx.ApiTls.Ca)
if conf.Nginx.API != nil && conf.Nginx.API.TLS.Ca != "" && conf.IsFileAllowed(conf.Nginx.API.TLS.Ca) {
data, err := os.ReadFile(conf.Nginx.API.TLS.Ca)
if err != nil {
log.Errorf("Unable to collect NGINX Plus metrics. Failed to read NGINX CA certificate file: %v", err)
return nil
Expand Down
29 changes: 17 additions & 12 deletions src/core/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,20 +220,25 @@ func (n *NginxBinaryType) GetNginxDetailsFromProcess(nginxProcess *Process) *pro
n.statusUrlMutex.RUnlock()

if urlsLength == 0 || nginxStatus == "" {
stubStatusApiUrl, err := sdk.GetStubStatusApiUrl(nginxDetailsFacade.ConfPath, n.config.IgnoreDirectives)
if err != nil {
log.Tracef("Unable to get Stub Status API URL from the configuration: NGINX OSS metrics will be unavailable for this system. please configure a Stub Status API to get NGINX OSS metrics: %v", err)
}
// If NGINX API URL is configured in agent config then use that istead of discovering the URL from the NGINX configuration
if n.config.Nginx.API != nil && n.config.Nginx.API.URL != "" {
nginxDetailsFacade.StatusUrl = n.config.Nginx.API.URL
} else {
stubStatusApiUrl, err := sdk.GetStubStatusApiUrl(nginxDetailsFacade.ConfPath, n.config.IgnoreDirectives)
if err != nil {
log.Tracef("Unable to get Stub Status API URL from the configuration: NGINX OSS metrics will be unavailable for this system. please configure a Stub Status API to get NGINX OSS metrics: %v", err)
}

nginxPlusApiUrl, err := sdk.GetNginxPlusApiUrl(nginxDetailsFacade.ConfPath, n.config.IgnoreDirectives)
if err != nil {
log.Tracef("Unable to get NGINX Plus API URL from the configuration: NGINX Plus metrics will be unavailable for this system. please configure a NGINX Plus API to get NGINX Plus metrics: %v", err)
}
nginxPlusApiUrl, err := sdk.GetNginxPlusApiUrl(nginxDetailsFacade.ConfPath, n.config.IgnoreDirectives)
if err != nil {
log.Tracef("Unable to get NGINX Plus API URL from the configuration: NGINX Plus metrics will be unavailable for this system. please configure a NGINX Plus API to get NGINX Plus metrics: %v", err)
}

if nginxDetailsFacade.Plus.Enabled {
nginxDetailsFacade.StatusUrl = nginxPlusApiUrl
} else {
nginxDetailsFacade.StatusUrl = stubStatusApiUrl
if nginxDetailsFacade.Plus.Enabled {
nginxDetailsFacade.StatusUrl = nginxPlusApiUrl
} else {
nginxDetailsFacade.StatusUrl = stubStatusApiUrl
}
}

n.statusUrlMutex.Lock()
Expand Down
213 changes: 213 additions & 0 deletions src/core/nginx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"

"github.com/nginx/agent/sdk/v2/proto"
"github.com/nginx/agent/sdk/v2/zip"
Expand Down Expand Up @@ -1499,3 +1500,215 @@ func TestGenerateActionMaps(t *testing.T) {
})
}
}

func TestGetNginxDetailsFromProcess(t *testing.T) {
// Test the core parsing and transformation logic by using the lower-level functions
testCases := []struct {
name string
process *Process
nginxInfo *nginxInfo
config *config.Config
expectedVersion string
expectedConfPath string
expectedProcessId string
expectedPlus bool
expectedPlusVersion string
expectedStatusUrl string
}{
{
name: "Test 1: OSS nginx process",
process: &Process{
Pid: 1234,
CreateTime: 1640995200,
Path: "/usr/sbin/nginx",
Command: "/usr/sbin/nginx",
IsMaster: true,
},
nginxInfo: &nginxInfo{
version: "1.20.2",
plusver: "",
source: "built by gcc 7.5.0",
prefix: "/etc/nginx",
confPath: "/etc/nginx/nginx.conf",
ssl: []string{"OpenSSL", "1.1.1f", "31 Mar 2020"},
cfgf: map[string]interface{}{
"prefix": "/etc/nginx",
"conf-path": "/etc/nginx/nginx.conf",
},
configureArgs: []string{"--prefix=/etc/nginx", "--with-http_ssl_module"},
mtime: time.Now(),
},
config: &config.Config{
Nginx: config.Nginx{},
IgnoreDirectives: []string{},
},
expectedVersion: "1.20.2",
expectedConfPath: "/etc/nginx/nginx.conf",
expectedProcessId: "1234",
expectedPlus: false,
expectedPlusVersion: "",
expectedStatusUrl: "", // Empty because no stub_status configured
},
{
name: "Test 2: Nginx Plus process",
process: &Process{
Pid: 5678,
CreateTime: 1640995300,
Path: "/usr/sbin/nginx",
Command: "/usr/sbin/nginx",
IsMaster: true,
},
nginxInfo: &nginxInfo{
version: "1.21.6",
plusver: "R26",
source: "built by gcc 9.4.0",
prefix: "/etc/nginx",
confPath: "/etc/nginx/nginx.conf",
ssl: []string{"OpenSSL", "1.1.1k", "25 Mar 2021"},
cfgf: map[string]interface{}{
"prefix": "/etc/nginx",
"conf-path": "/etc/nginx/nginx.conf",
},
configureArgs: []string{"--prefix=/etc/nginx", "--with-http_v2_module"},
mtime: time.Now(),
},
config: &config.Config{
Nginx: config.Nginx{},
IgnoreDirectives: []string{},
},
expectedVersion: "1.21.6",
expectedConfPath: "/etc/nginx/nginx.conf",
expectedProcessId: "5678",
expectedPlus: true,
expectedPlusVersion: "R26",
expectedStatusUrl: "", // Empty because no Plus API configured
},
{
name: "Test 3: Process with custom conf path from command line",
process: &Process{
Pid: 9012,
CreateTime: 1640995400,
Path: "/opt/nginx/sbin/nginx",
Command: "/opt/nginx/sbin/nginx -c /custom/nginx.conf",
IsMaster: true,
},
nginxInfo: &nginxInfo{
version: "1.22.1",
plusver: "",
prefix: "/opt/nginx",
confPath: "/opt/nginx/conf/nginx.conf",
ssl: []string{"BoringSSL"},
cfgf: map[string]interface{}{
"prefix": "/opt/nginx",
"conf-path": "/opt/nginx/conf/nginx.conf",
},
configureArgs: []string{"--prefix=/opt/nginx"},
mtime: time.Now(),
},
config: &config.Config{
Nginx: config.Nginx{},
IgnoreDirectives: []string{},
},
expectedVersion: "1.22.1",
expectedConfPath: "/custom/nginx.conf", // Should be overridden by command line
expectedProcessId: "9012",
expectedPlus: false,
expectedPlusVersion: "",
expectedStatusUrl: "", // Empty because no stub_status configured
},
{
name: "Test 4: Process with configured API URL",
process: &Process{
Pid: 1111,
CreateTime: 1640995500,
Path: "/usr/sbin/nginx",
Command: "/usr/sbin/nginx",
IsMaster: true,
},
nginxInfo: &nginxInfo{
version: "1.20.2",
plusver: "",
source: "built by gcc 7.5.0",
prefix: "/etc/nginx",
confPath: "/etc/nginx/nginx.conf",
ssl: []string{"OpenSSL", "1.1.1f", "31 Mar 2020"},
cfgf: map[string]interface{}{
"prefix": "/etc/nginx",
"conf-path": "/etc/nginx/nginx.conf",
},
configureArgs: []string{"--prefix=/etc/nginx", "--with-http_ssl_module"},
mtime: time.Now(),
},
config: &config.Config{
Nginx: config.Nginx{
API: &config.NginxAPI{
URL: "http://127.0.0.1:8080/api",
},
},
IgnoreDirectives: []string{},
},
expectedVersion: "1.20.2",
expectedConfPath: "/etc/nginx/nginx.conf",
expectedProcessId: "1111",
expectedPlus: false,
expectedPlusVersion: "",
expectedStatusUrl: "http://127.0.0.1:8080/api", // From agent config
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a function to test the core logic without file system dependencies
nginxBinary := &NginxBinaryType{
config: tc.config,
statusUrls: make(map[string]string),
}

// Mock the getNginxInfoFrom method by creating a wrapper that returns our test data
originalPath := tc.process.Path

// Create temporary file to satisfy the os.Stat call in the cached path
tmpFile := filepath.Join(t.TempDir(), "nginx")
err := os.WriteFile(tmpFile, []byte(""), 0o755)
require.NoError(t, err)

// Set modification time to match our test data
err = os.Chtimes(tmpFile, tc.nginxInfo.mtime, tc.nginxInfo.mtime)
require.NoError(t, err)

// Update process path and cache the nginx info to avoid nginx -V call
tc.process.Path = tmpFile
if nginxBinary.nginxInfoMap == nil {
nginxBinary.nginxInfoMap = make(map[string]*nginxInfo)
}
nginxBinary.nginxInfoMap[tmpFile] = tc.nginxInfo

// Call the function under test
result := nginxBinary.GetNginxDetailsFromProcess(tc.process)

// Restore original path for consistent testing
tc.process.Path = originalPath

// Validate core fields
assert.Equal(t, tc.expectedVersion, result.Version)
assert.Equal(t, tc.expectedConfPath, result.ConfPath)
assert.Equal(t, tc.expectedProcessId, result.ProcessId)

// Test Plus-specific assertions
require.NotNil(t, result.Plus)
assert.Equal(t, tc.expectedPlus, result.Plus.Enabled)
assert.Equal(t, tc.expectedPlusVersion, result.Plus.Release)

// Validate StatusUrl
assert.Equal(t, tc.expectedStatusUrl, result.StatusUrl)

// Validate NginxId is generated
assert.NotEmpty(t, result.NginxId)

// Validate other core fields are set correctly
assert.Equal(t, fmt.Sprintf("%d", tc.process.Pid), result.ProcessId)
assert.Equal(t, tc.process.CreateTime, result.StartTime)
assert.Equal(t, false, result.BuiltFromSource) // Default value
})
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading