Skip to content

Commit 2ba165d

Browse files
authored
Check NGINX worker processes to check if NGINX reload is complete (#1207)
1 parent c84a2bf commit 2ba165d

23 files changed

+1215
-296
lines changed

api/grpc/mpi/v1/command.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/grpc/mpi/v1/common.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/grpc/mpi/v1/files.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/config/config.go

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -379,28 +379,6 @@ func registerFlags() {
379379
DefManifestDir,
380380
"Specifies the path to the directory containing the manifest files",
381381
)
382-
fs.Duration(
383-
NginxReloadMonitoringPeriodKey,
384-
DefNginxReloadMonitoringPeriod,
385-
"The amount of time to monitor NGINX after a reload of configuration.",
386-
)
387-
fs.Bool(
388-
NginxTreatWarningsAsErrorsKey,
389-
DefTreatErrorsAsWarnings,
390-
"Warning messages in the NGINX errors logs after a NGINX reload will be treated as an error.",
391-
)
392-
393-
fs.String(
394-
NginxApiTlsCa,
395-
DefNginxApiTlsCa,
396-
"The NGINX Plus CA certificate file location needed to call the NGINX Plus API if SSL is enabled.",
397-
)
398-
399-
fs.StringSlice(
400-
NginxExcludeLogsKey, []string{},
401-
"A comma-separated list of one or more NGINX log paths that you want to exclude from metrics "+
402-
"collection or error monitoring. This includes absolute paths or regex patterns",
403-
)
404382

405383
fs.StringSlice(AllowedDirectoriesKey,
406384
DefaultAllowedDirectories(),
@@ -442,6 +420,7 @@ func registerFlags() {
442420
registerAuxiliaryCommandFlags(fs)
443421
registerCollectorFlags(fs)
444422
registerClientFlags(fs)
423+
registerDataPlaneFlags(fs)
445424

446425
fs.SetNormalizeFunc(normalizeFunc)
447426

@@ -456,6 +435,57 @@ func registerFlags() {
456435
})
457436
}
458437

438+
func registerDataPlaneFlags(fs *flag.FlagSet) {
439+
fs.Duration(
440+
NginxReloadMonitoringPeriodKey,
441+
DefNginxReloadMonitoringPeriod,
442+
"The amount of time to monitor NGINX after a reload of configuration.",
443+
)
444+
fs.Bool(
445+
NginxTreatWarningsAsErrorsKey,
446+
DefTreatErrorsAsWarnings,
447+
"Warning messages in the NGINX errors logs after a NGINX reload will be treated as an error.",
448+
)
449+
450+
fs.String(
451+
NginxApiTlsCa,
452+
DefNginxApiTlsCa,
453+
"The NGINX Plus CA certificate file location needed to call the NGINX Plus API if SSL is enabled.",
454+
)
455+
456+
fs.StringSlice(
457+
NginxExcludeLogsKey, []string{},
458+
"A comma-separated list of one or more NGINX log paths that you want to exclude from metrics "+
459+
"collection or error monitoring. This includes absolute paths or regex patterns",
460+
)
461+
462+
// NGINX Reload Backoff Flags
463+
fs.Duration(
464+
NginxReloadBackoffInitialIntervalKey,
465+
DefNginxReloadBackoffInitialInterval,
466+
"The NGINX reload backoff initial interval, value in seconds")
467+
468+
fs.Duration(
469+
NginxReloadBackoffMaxIntervalKey,
470+
DefNginxReloadBackoffMaxInterval,
471+
"The NGINX reload backoff max interval, value in seconds")
472+
473+
fs.Duration(
474+
NginxReloadBackoffMaxElapsedTimeKey,
475+
DefNginxReloadBackoffMaxElapsedTime,
476+
"The NGINX reload backoff max elapsed time, value in seconds")
477+
478+
fs.Float64(
479+
NginxReloadBackoffRandomizationFactorKey,
480+
DefNginxReloadBackoffRandomizationFactor,
481+
"The NGINX reload backoff randomization factor, value float")
482+
483+
fs.Float64(
484+
NginxReloadBackoffMultiplierKey,
485+
DefNginxReloadBackoffMultiplier,
486+
"The NGINX reload backoff multiplier, value float")
487+
}
488+
459489
func registerCommonFlags(fs *flag.FlagSet) {
460490
fs.StringToString(
461491
LabelsRootKey,
@@ -906,6 +936,13 @@ func resolveDataPlaneConfig() *DataPlaneConfig {
906936
TreatWarningsAsErrors: viperInstance.GetBool(NginxTreatWarningsAsErrorsKey),
907937
ExcludeLogs: viperInstance.GetStringSlice(NginxExcludeLogsKey),
908938
APITls: TLSConfig{Ca: viperInstance.GetString(NginxApiTlsCa)},
939+
ReloadBackoff: &BackOff{
940+
InitialInterval: viperInstance.GetDuration(NginxReloadBackoffInitialIntervalKey),
941+
MaxInterval: viperInstance.GetDuration(NginxReloadBackoffMaxIntervalKey),
942+
MaxElapsedTime: viperInstance.GetDuration(NginxReloadBackoffMaxElapsedTimeKey),
943+
RandomizationFactor: viperInstance.GetFloat64(NginxReloadBackoffRandomizationFactorKey),
944+
Multiplier: viperInstance.GetFloat64(NginxReloadBackoffMultiplierKey),
945+
},
909946
},
910947
}
911948
}

internal/config/config_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,13 @@ func createConfig() *Config {
11011101
ExcludeLogs: []string{"/var/log/nginx/error.log", "^/var/log/nginx/.*.log$"},
11021102
ReloadMonitoringPeriod: 30 * time.Second,
11031103
TreatWarningsAsErrors: true,
1104+
ReloadBackoff: &BackOff{
1105+
InitialInterval: 100 * time.Millisecond,
1106+
MaxInterval: 20 * time.Second,
1107+
MaxElapsedTime: 15 * time.Second,
1108+
RandomizationFactor: 1.5,
1109+
Multiplier: 1.5,
1110+
},
11041111
},
11051112
},
11061113
Collector: &Collector{

internal/config/defaults.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ const (
1616
DefTreatErrorsAsWarnings = false
1717
DefNginxApiTlsCa = ""
1818

19+
// Nginx Reload Backoff defaults
20+
DefNginxReloadBackoffInitialInterval = 1 * time.Second
21+
DefNginxReloadBackoffRandomizationFactor = 0.5 // the value is 0 <= and < 1
22+
DefNginxReloadBackoffMultiplier = 5
23+
DefNginxReloadBackoffMaxInterval = 10 * time.Second
24+
DefNginxReloadBackoffMaxElapsedTime = 30 * time.Second
25+
1926
DefCommandServerHostKey = ""
2027
DefCommandServerPortKey = 0
2128
DefCommandServerTypeKey = "grpc"

internal/config/flags.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,16 @@ var (
112112
LogLevelKey = pre(LogLevelRootKey) + "level"
113113
LogPathKey = pre(LogLevelRootKey) + "path"
114114

115-
NginxReloadMonitoringPeriodKey = pre(DataPlaneConfigRootKey, "nginx") + "reload_monitoring_period"
116-
NginxTreatWarningsAsErrorsKey = pre(DataPlaneConfigRootKey, "nginx") + "treat_warnings_as_errors"
117-
NginxExcludeLogsKey = pre(DataPlaneConfigRootKey, "nginx") + "exclude_logs"
118-
NginxApiTlsCa = pre(DataPlaneConfigRootKey, "nginx") + "api_tls_ca"
115+
NginxReloadMonitoringPeriodKey = pre(DataPlaneConfigRootKey, "nginx") + "reload_monitoring_period"
116+
NginxTreatWarningsAsErrorsKey = pre(DataPlaneConfigRootKey, "nginx") + "treat_warnings_as_errors"
117+
NginxReloadBackoffKey = pre(DataPlaneConfigRootKey, "nginx") + "reload_backoff"
118+
NginxReloadBackoffInitialIntervalKey = pre(NginxReloadBackoffKey) + "initial_interval"
119+
NginxReloadBackoffMaxIntervalKey = pre(NginxReloadBackoffKey) + "max_interval"
120+
NginxReloadBackoffMaxElapsedTimeKey = pre(NginxReloadBackoffKey) + "max_elapsed_time"
121+
NginxReloadBackoffRandomizationFactorKey = pre(NginxReloadBackoffKey) + "randomization_factor"
122+
NginxReloadBackoffMultiplierKey = pre(NginxReloadBackoffKey) + "multiplier"
123+
NginxExcludeLogsKey = pre(DataPlaneConfigRootKey, "nginx") + "exclude_logs"
124+
NginxApiTlsCa = pre(DataPlaneConfigRootKey, "nginx") + "api_tls_ca"
119125

120126
FileWatcherMonitoringFrequencyKey = pre(FileWatcherKey) + "monitoring_frequency"
121127
NginxExcludeFilesKey = pre(FileWatcherKey) + "exclude_files"

internal/config/testdata/nginx-agent.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ data_plane_config:
3131
exclude_logs:
3232
- /var/log/nginx/error.log
3333
- ^/var/log/nginx/.*.log$
34+
reload_backoff:
35+
initial_interval: 100ms
36+
max_interval: 20s
37+
max_elapsed_time: 15s
38+
randomization_factor: 1.5
39+
multiplier: 1.5
3440
client:
3541
http:
3642
timeout: 15s

internal/config/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type (
6363
}
6464

6565
NginxDataPlaneConfig struct {
66+
ReloadBackoff *BackOff `yaml:"reload_backoff" mapstructure:"reload_backoff"`
6667
APITls TLSConfig `yaml:"api_tls" mapstructure:"api_tls"`
6768
ExcludeLogs []string `yaml:"exclude_logs" mapstructure:"exclude_logs"`
6869
ReloadMonitoringPeriod time.Duration `yaml:"reload_monitoring_period" mapstructure:"reload_monitoring_period"`

internal/datasource/nginx/process.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright (c) F5, Inc.
2+
//
3+
// This source code is licensed under the Apache License, Version 2.0 license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
package nginx
7+
8+
import (
9+
"bufio"
10+
"bytes"
11+
"context"
12+
"fmt"
13+
"regexp"
14+
"strings"
15+
16+
"github.com/nginx/agent/v3/internal/datasource/host/exec"
17+
"github.com/nginx/agent/v3/internal/model"
18+
"github.com/nginx/agent/v3/pkg/nginxprocess"
19+
)
20+
21+
const (
22+
keyValueLen = 2
23+
flagLen = 1
24+
)
25+
26+
var versionRegex = regexp.MustCompile(`(?P<name>\S+)\/(?P<version>.*)`)
27+
28+
func ProcessInfo(ctx context.Context, proc *nginxprocess.Process,
29+
executer exec.ExecInterface,
30+
) (*model.ProcessInfo, error) {
31+
exePath := proc.Exe
32+
33+
if exePath == "" {
34+
exePath = Exe(ctx, executer)
35+
if exePath == "" {
36+
return nil, fmt.Errorf("unable to find NGINX exe for process %d", proc.PID)
37+
}
38+
}
39+
40+
confPath := ConfPathFromCommand(proc.Cmd)
41+
42+
var nginxInfo *model.ProcessInfo
43+
44+
outputBuffer, err := executer.RunCmd(ctx, exePath, "-V")
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
nginxInfo = ParseNginxVersionCommandOutput(ctx, outputBuffer)
50+
51+
nginxInfo.ExePath = exePath
52+
nginxInfo.ProcessID = proc.PID
53+
54+
if nginxInfo.ConfPath = model.NginxConfPath(ctx, nginxInfo); confPath != "" {
55+
nginxInfo.ConfPath = confPath
56+
}
57+
58+
return nginxInfo, err
59+
}
60+
61+
func Exe(ctx context.Context, executer exec.ExecInterface) string {
62+
exePath := ""
63+
64+
out, commandErr := executer.RunCmd(ctx, "sh", "-c", "command -v nginx")
65+
if commandErr == nil {
66+
exePath = strings.TrimSuffix(out.String(), "\n")
67+
}
68+
69+
if exePath == "" {
70+
exePath = defaultToNginxCommandForProcessPath(executer)
71+
}
72+
73+
if strings.Contains(exePath, "(deleted)") {
74+
exePath = sanitizeExeDeletedPath(exePath)
75+
}
76+
77+
return exePath
78+
}
79+
80+
func defaultToNginxCommandForProcessPath(executer exec.ExecInterface) string {
81+
exePath, err := executer.FindExecutable("nginx")
82+
if err != nil {
83+
return ""
84+
}
85+
86+
return exePath
87+
}
88+
89+
func sanitizeExeDeletedPath(exe string) string {
90+
firstSpace := strings.Index(exe, "(deleted)")
91+
if firstSpace != -1 {
92+
return strings.TrimSpace(exe[0:firstSpace])
93+
}
94+
95+
return strings.TrimSpace(exe)
96+
}
97+
98+
func ConfPathFromCommand(command string) string {
99+
commands := strings.Split(command, " ")
100+
101+
for i, command := range commands {
102+
if command == "-c" {
103+
if i < len(commands)-1 {
104+
return commands[i+1]
105+
}
106+
}
107+
}
108+
109+
return ""
110+
}
111+
112+
func ParseNginxVersionCommandOutput(ctx context.Context, output *bytes.Buffer) *model.ProcessInfo {
113+
nginxInfo := &model.ProcessInfo{}
114+
115+
scanner := bufio.NewScanner(output)
116+
for scanner.Scan() {
117+
line := strings.TrimSpace(scanner.Text())
118+
switch {
119+
case strings.HasPrefix(line, "nginx version"):
120+
nginxInfo.Version = parseNginxVersion(line)
121+
case strings.HasPrefix(line, "configure arguments"):
122+
nginxInfo.ConfigureArgs = parseConfigureArguments(line)
123+
}
124+
}
125+
126+
nginxInfo.Prefix = model.NginxPrefix(ctx, nginxInfo)
127+
128+
return nginxInfo
129+
}
130+
131+
func parseNginxVersion(line string) string {
132+
return strings.TrimPrefix(versionRegex.FindString(line), "nginx/")
133+
}
134+
135+
func parseConfigureArguments(line string) map[string]interface{} {
136+
// need to check for empty strings
137+
flags := strings.Split(line[len("configure arguments:"):], " --")
138+
result := make(map[string]interface{})
139+
140+
for _, flag := range flags {
141+
vals := strings.Split(flag, "=")
142+
if isFlag(vals) {
143+
result[vals[0]] = true
144+
} else if isKeyValueFlag(vals) {
145+
result[vals[0]] = vals[1]
146+
}
147+
}
148+
149+
return result
150+
}
151+
152+
func isFlag(vals []string) bool {
153+
return len(vals) == flagLen && vals[0] != ""
154+
}
155+
156+
func isKeyValueFlag(vals []string) bool {
157+
return len(vals) == keyValueLen
158+
}

0 commit comments

Comments
 (0)