diff --git a/internal/collector/nginxossreceiver/internal/config/config.go b/internal/collector/nginxossreceiver/internal/config/config.go index 3d272c1a4..67b90c68d 100644 --- a/internal/collector/nginxossreceiver/internal/config/config.go +++ b/internal/collector/nginxossreceiver/internal/config/config.go @@ -24,6 +24,7 @@ const ( type Config struct { confighttp.ClientConfig `mapstructure:",squash"` APIDetails APIDetails `mapstructure:"api_details"` + InstanceID string `mapstructure:"instance_id"` AccessLogs []AccessLog `mapstructure:"access_logs"` MetricsBuilderConfig metadata.MetricsBuilderConfig `mapstructure:",squash"` scraperhelper.ControllerConfig `mapstructure:",squash"` diff --git a/internal/collector/nginxossreceiver/internal/scraper/accesslog/nginx_log_scraper.go b/internal/collector/nginxossreceiver/internal/scraper/accesslog/nginx_log_scraper.go index a1e3cf64f..92fbce0a2 100644 --- a/internal/collector/nginxossreceiver/internal/scraper/accesslog/nginx_log_scraper.go +++ b/internal/collector/nginxossreceiver/internal/scraper/accesslog/nginx_log_scraper.go @@ -171,7 +171,7 @@ func (nls *NginxLogScraper) Scrape(_ context.Context) (pmetric.Metrics, error) { nls.entries = make([]*entry.Entry, 0) timeNow := pcommon.NewTimestampFromTime(time.Now()) - nls.rb.SetInstanceID(nls.settings.ID.Name()) + nls.rb.SetInstanceID(nls.cfg.InstanceID) nls.rb.SetInstanceType("nginx") nls.logger.Debug("NGINX OSS access log resource info", zap.Any("resource", nls.rb)) diff --git a/internal/collector/nginxossreceiver/internal/scraper/stubstatus/stub_status_scraper.go b/internal/collector/nginxossreceiver/internal/scraper/stubstatus/stub_status_scraper.go index 15b61ec93..4d3002243 100644 --- a/internal/collector/nginxossreceiver/internal/scraper/stubstatus/stub_status_scraper.go +++ b/internal/collector/nginxossreceiver/internal/scraper/stubstatus/stub_status_scraper.go @@ -138,7 +138,7 @@ func (s *NginxStubStatusScraper) Scrape(context.Context) (pmetric.Metrics, error return pmetric.Metrics{}, err } - s.rb.SetInstanceID(s.settings.ID.Name()) + s.rb.SetInstanceID(s.cfg.InstanceID) s.rb.SetInstanceType("nginx") s.settings.Logger.Debug("NGINX OSS stub status resource info", zap.Any("resource", s.rb)) diff --git a/internal/collector/nginxplusreceiver/config.go b/internal/collector/nginxplusreceiver/config.go index 6e622aaea..d633d4fee 100644 --- a/internal/collector/nginxplusreceiver/config.go +++ b/internal/collector/nginxplusreceiver/config.go @@ -21,6 +21,7 @@ const defaultCollectInterval = 10 * time.Second type Config struct { confighttp.ClientConfig `mapstructure:",squash"` APIDetails APIDetails `mapstructure:"api_details"` + InstanceID string `mapstructure:"instance_id"` MetricsBuilderConfig metadata.MetricsBuilderConfig `mapstructure:",squash"` scraperhelper.ControllerConfig `mapstructure:",squash"` } diff --git a/internal/collector/nginxplusreceiver/scraper.go b/internal/collector/nginxplusreceiver/scraper.go index 165545e26..4aa151c9e 100644 --- a/internal/collector/nginxplusreceiver/scraper.go +++ b/internal/collector/nginxplusreceiver/scraper.go @@ -127,7 +127,7 @@ func (nps *NginxPlusScraper) Scrape(ctx context.Context) (pmetric.Metrics, error return pmetric.Metrics{}, fmt.Errorf("failed to get stats from plus API: %w", err) } - nps.rb.SetInstanceID(nps.settings.ID.Name()) + nps.rb.SetInstanceID(nps.cfg.InstanceID) nps.rb.SetInstanceType("nginxplus") nps.logger.Debug("NGINX Plus resource info", zap.Any("resource", nps.rb)) diff --git a/internal/collector/otel_collector_plugin.go b/internal/collector/otel_collector_plugin.go index 5112e68be..9daad0ccd 100644 --- a/internal/collector/otel_collector_plugin.go +++ b/internal/collector/otel_collector_plugin.go @@ -16,7 +16,9 @@ import ( "sync" "time" + "github.com/goccy/go-yaml" pkgConfig "github.com/nginx/agent/v3/pkg/config" + "go.opentelemetry.io/collector/confmap" "github.com/nginx/agent/v3/api/grpc/mpi/v1" "github.com/nginx/agent/v3/internal/backoff" @@ -384,6 +386,31 @@ func (oc *Collector) updateHeadersSetterExtension( return headersSetterExtensionUpdated } +func (oc *Collector) writeRunningConfig(ctx context.Context, settings otelcol.CollectorSettings) error { + slog.DebugContext(ctx, "Writing running OTel collector config", "path", + "/var/lib/nginx-agent/opentelemetry-collector-agent-debug.yaml") + resolver, err := confmap.NewResolver(settings.ConfigProviderSettings.ResolverSettings) + if err != nil { + return fmt.Errorf("unable to create resolver: %w", err) + } + + con, err := resolver.Resolve(ctx) + if err != nil { + return fmt.Errorf("error while resolving config: %w", err) + } + b, err := yaml.Marshal(con.ToStringMap()) + if err != nil { + return fmt.Errorf("error while marshaling to YAML: %w", err) + } + + writeErr := os.WriteFile("/var/lib/nginx-agent/opentelemetry-collector-agent-debug.yaml", b, filePermission) + if writeErr != nil { + return fmt.Errorf("error while writing debug config: %w", err) + } + + return nil +} + func (oc *Collector) restartCollector(ctx context.Context) { err := oc.Close(ctx) if err != nil { @@ -392,6 +419,14 @@ func (oc *Collector) restartCollector(ctx context.Context) { } settings := OTelCollectorSettings(oc.config) + + if strings.ToLower(oc.config.Log.Level) == "debug" { + writeErr := oc.writeRunningConfig(ctx, settings) + if writeErr != nil { + slog.ErrorContext(ctx, "Failed to write debug OTel Collector config", "error", writeErr) + } + } + oTelCollector, err := otelcol.NewCollector(settings) if err != nil { slog.ErrorContext(ctx, "Failed to create OTel Collector", "error", err) diff --git a/internal/collector/otelcol.tmpl b/internal/collector/otelcol.tmpl index 49e46cf23..e0b1015f1 100644 --- a/internal/collector/otelcol.tmpl +++ b/internal/collector/otelcol.tmpl @@ -79,7 +79,12 @@ receivers: {{- end }} {{- range .Receivers.NginxReceivers }} +{{- if gt (len $.Receivers.NginxReceivers) 1 }} nginx/{{- .InstanceID -}}: +{{- else }} + nginx: +{{- end}} + instance_id: "{{- .InstanceID -}}" api_details: url: "{{- .StubStatus.URL -}}" listen: "{{- .StubStatus.Listen -}}" @@ -98,12 +103,17 @@ receivers: {{- end }} {{- range .Receivers.NginxPlusReceivers }} +{{- if gt (len $.Receivers.NginxPlusReceivers) 1 }} nginxplus/{{- .InstanceID -}}: +{{- else }} + nginxplus: +{{- end}} + instance_id: "{{- .InstanceID -}}" api_details: - url: "{{- .PlusAPI.URL -}}" - listen: "{{- .PlusAPI.Listen -}}" - location: "{{- .PlusAPI.Location -}}" - ca: "{{- .PlusAPI.Ca -}}" + url: "{{- .PlusAPI.URL -}}" + listen: "{{- .PlusAPI.Listen -}}" + location: "{{- .PlusAPI.Location -}}" + ca: "{{- .PlusAPI.Ca -}}" {{- if .CollectionInterval }} collection_interval: {{ .CollectionInterval }} {{- end }} @@ -274,10 +284,18 @@ service: {{- end }} {{- else if eq $receiver "nginx_metrics" }} {{- range $.Receivers.NginxReceivers }} + {{- if gt (len $.Receivers.NginxReceivers) 1 }} - nginx/{{- .InstanceID -}} + {{- else }} + - nginx + {{- end }} {{- end }} {{- range $.Receivers.NginxPlusReceivers }} + {{- if gt (len $.Receivers.NginxReceivers) 1 }} - nginxplus/{{- .InstanceID -}} + {{- else }} + - nginxplus + {{- end }} {{- end }} {{- else }} - {{ $receiver }} diff --git a/internal/collector/settings.go b/internal/collector/settings.go index e89bb89d6..11a7fc8a4 100644 --- a/internal/collector/settings.go +++ b/internal/collector/settings.go @@ -71,7 +71,11 @@ func createConverterFactories() []confmap.ConverterFactory { } func createURIs(cfg *config.Config) []string { - return []string{cfg.Collector.ConfigPath} + configFiles := []string{cfg.Collector.ConfigPath} + configFiles = slices.Concat(configFiles, cfg.Collector.AdditionalPaths) + slog.Info("Additional config files:", "", configFiles) + + return configFiles } func createFile(confPath string) error { diff --git a/internal/collector/settings_test.go b/internal/collector/settings_test.go index 0d7b5ce34..d152d7bfe 100644 --- a/internal/collector/settings_test.go +++ b/internal/collector/settings_test.go @@ -7,6 +7,7 @@ package collector import ( "os" "path/filepath" + "slices" "testing" "time" @@ -107,6 +108,28 @@ func TestTemplateWrite(t *testing.T) { }, }, }) + + cfg.Collector.Receivers.NginxPlusReceivers = slices.Concat(cfg.Collector.Receivers.NginxPlusReceivers, + []config.NginxPlusReceiver{ + { + InstanceID: "456", + PlusAPI: config.APIDetails{ + URL: "http://localhost:80/api", + Listen: "", + Location: "", + }, + CollectionInterval: 20 * time.Second, + }, + { + InstanceID: "789", + PlusAPI: config.APIDetails{ + URL: "http://localhost:90/api", + Listen: "", + Location: "", + }, + CollectionInterval: 20 * time.Second, + }, + }) // Clear default config and test collector with TLS enabled cfg.Collector.Receivers.OtlpReceivers["default"] = &config.OtlpReceiver{ Server: &config.ServerConfig{ @@ -165,7 +188,10 @@ func TestTemplateWrite(t *testing.T) { cfg.Collector.Pipelines.Metrics = make(map[string]*config.Pipeline) cfg.Collector.Pipelines.Metrics["default"] = &config.Pipeline{ - Receivers: []string{"hostmetrics", "containermetrics", "otlp/default", "nginx/123"}, + Receivers: []string{ + "hostmetrics", "containermetrics", + "otlp/default", "nginx", "nginxplus/456", "nginxplus/789", + }, Processors: []string{"resource/default", "batch/default"}, Exporters: []string{"otlp/default", "prometheus", "debug"}, } diff --git a/internal/config/config.go b/internal/config/config.go index cc72af59c..5c1a0a0f1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -755,6 +755,14 @@ func registerCollectorFlags(fs *flag.FlagSet) { "The path to the Opentelemetry Collector configuration file.", ) + fs.StringSlice( + CollectorAdditionalConfigPathsKey, + []string{}, + "Paths to additional OpenTelemetry Collector configuration files. The order of the configuration files"+ + " determines which config file takes priority. The last config file will take precedent over other files "+ + "if they have the same setting. Paths to configuration files must be absolute", + ) + fs.String( CollectorLogLevelKey, DefCollectorLogLevel, @@ -1053,13 +1061,14 @@ func resolveCollector(allowedDirs []string) (*Collector, error) { } col := &Collector{ - ConfigPath: viperInstance.GetString(CollectorConfigPathKey), - Exporters: exporters, - Processors: resolveProcessors(), - Receivers: receivers, - Extensions: resolveExtensions(), - Log: resolveCollectorLog(), - Pipelines: resolvePipelines(), + ConfigPath: viperInstance.GetString(CollectorConfigPathKey), + AdditionalPaths: viperInstance.GetStringSlice(CollectorAdditionalConfigPathsKey), + Exporters: exporters, + Processors: resolveProcessors(), + Receivers: receivers, + Extensions: resolveExtensions(), + Log: resolveCollectorLog(), + Pipelines: resolvePipelines(), } // Check for self-signed certificate true in Agent conf diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f55afee23..59d602aaa 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -64,6 +64,7 @@ func TestResolveConfig(t *testing.T) { actual, err := ResolveConfig() require.NoError(t, err) + t.Logf("Actual: %+v", actual.AllowedDirectories) sort.Slice(actual.Collector.Extensions.HeadersSetter.Headers, func(i, j int) bool { headers := actual.Collector.Extensions.HeadersSetter.Headers return headers[i].Key < headers[j].Key @@ -1094,7 +1095,7 @@ func createConfig() *Config { }, AllowedDirectories: []string{ "/etc/nginx-agent", "/etc/nginx", "/usr/local/etc/nginx", "/var/run/nginx", - "/usr/share/nginx/modules", "/var/log/nginx", + "/usr/share/nginx/modules", "/var/log/nginx", "/configs", }, DataPlaneConfig: &DataPlaneConfig{ Nginx: &NginxDataPlaneConfig{ @@ -1111,7 +1112,8 @@ func createConfig() *Config { }, }, Collector: &Collector{ - ConfigPath: "/etc/nginx-agent/nginx-agent-otelcol.yaml", + ConfigPath: "/etc/nginx-agent/nginx-agent-otelcol.yaml", + AdditionalPaths: []string{"/configs/my_config.yaml", "/etc/nginx-agent/nginx-agent-otelcol.yaml"}, Exporters: Exporters{ OtlpExporters: map[string]*OtlpExporter{ "default": { diff --git a/internal/config/flags.go b/internal/config/flags.go index 3e51eb52b..697c2906e 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -48,6 +48,7 @@ var ( ClientBackoffMultiplierKey = pre(ClientRootKey) + "backoff_multiplier" CollectorConfigPathKey = pre(CollectorRootKey) + "config_path" + CollectorAdditionalConfigPathsKey = pre(CollectorRootKey) + "additional_config_paths" CollectorExportersKey = pre(CollectorRootKey) + "exporters" CollectorDebugExporterKey = pre(CollectorExportersKey) + "debug" CollectorPrometheusExporterKey = pre(CollectorExportersKey) + "prometheus" diff --git a/internal/config/testdata/nginx-agent.conf b/internal/config/testdata/nginx-agent.conf index 9df36198d..f3bd26207 100644 --- a/internal/config/testdata/nginx-agent.conf +++ b/internal/config/testdata/nginx-agent.conf @@ -63,6 +63,7 @@ allowed_directories: - /var/run/nginx - /usr/share/nginx/modules - /var/log/nginx + - /configs command: server: @@ -94,6 +95,9 @@ auxiliary_command: collector: config_path: "/etc/nginx-agent/nginx-agent-otelcol.yaml" + additional_config_paths: + - "/configs/my_config.yaml" + - /etc/nginx-agent/nginx-agent-otelcol.yaml receivers: otlp: "default": diff --git a/internal/config/types.go b/internal/config/types.go index a5d07a210..b599de9cd 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -105,13 +105,14 @@ type ( } Collector struct { - ConfigPath string `yaml:"config_path" mapstructure:"config_path"` - Log *Log `yaml:"log" mapstructure:"log"` - Exporters Exporters `yaml:"exporters" mapstructure:"exporters"` - Extensions Extensions `yaml:"extensions" mapstructure:"extensions"` - Processors Processors `yaml:"processors" mapstructure:"processors"` - Pipelines Pipelines `yaml:"pipelines" mapstructure:"pipelines"` - Receivers Receivers `yaml:"receivers" mapstructure:"receivers"` + ConfigPath string `yaml:"config_path" mapstructure:"config_path"` + AdditionalPaths []string `yaml:"additional_config_paths" mapstructure:"additional_config_paths"` + Log *Log `yaml:"log" mapstructure:"log"` + Exporters Exporters `yaml:"exporters" mapstructure:"exporters"` + Extensions Extensions `yaml:"extensions" mapstructure:"extensions"` + Processors Processors `yaml:"processors" mapstructure:"processors"` + Pipelines Pipelines `yaml:"pipelines" mapstructure:"pipelines"` + Receivers Receivers `yaml:"receivers" mapstructure:"receivers"` } Pipelines struct { @@ -364,6 +365,14 @@ func (col *Collector) Validate(allowedDirectories []string) error { err = errors.Join(err, nginxReceiver.Validate(allowedDirectories)) } + for _, path := range col.AdditionalPaths { + cleanPath := filepath.Clean(path) + pathAllowed := isAllowedDir(cleanPath, allowedDirectories) + if !pathAllowed { + err = errors.Join(err, fmt.Errorf("additional config path %s not in allowed directories", path)) + } + } + return err } diff --git a/test/config/collector/test-opentelemetry-collector-agent.yaml b/test/config/collector/test-opentelemetry-collector-agent.yaml index aa72f4a4b..5cb549bfb 100644 --- a/test/config/collector/test-opentelemetry-collector-agent.yaml +++ b/test/config/collector/test-opentelemetry-collector-agent.yaml @@ -26,7 +26,8 @@ receivers: cert_file: /tmp/cert.pem key_file: /tmp/key.pem ca_file: /tmp/ca.pem - nginx/123: + nginx: + instance_id: "123" api_details: url: "http://localhost:80/status" listen: "" @@ -36,6 +37,22 @@ receivers: access_logs: - log_format: "$remote_addr - $remote_user [$time_local] \"$request\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\" \"$http_x_forwarded_for\"\"$upstream_cache_status\"" file_path: "/var/log/nginx/access-custom.conf" + nginxplus/456: + instance_id: "456" + api_details: + url: "http://localhost:80/api" + listen: "" + location: "" + ca: "" + collection_interval: 20s + nginxplus/789: + instance_id: "789" + api_details: + url: "http://localhost:90/api" + listen: "" + location: "" + ca: "" + collection_interval: 20s tcplog/default: listen_address: "localhost:151" operators: @@ -108,7 +125,9 @@ service: - hostmetrics - containermetrics - otlp/default - - nginx/123 + - nginx + - nginxplus/456 + - nginxplus/789 processors: - resource/default - batch/default