Files
aac1a8f39c [confmap] Stabilize confmap validation facilities (#15142)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description

The `xconfmap.Validator` interface has existed in some form for a very
long time now and is ready to be stabilized. Similarly, the
`xconfmap.Validate` method has existed in it's current state for over a
year and doesn't have any open issues I'm aware of. It should also be
ready for stabilization.

Many modules depend on `xconfmap` merely for the validation facilities
(as can be seen by the go.mod diffs), so this will allow them to depend
on only stable modules.

---------

Co-authored-by: Alex Boten <[email protected]>
2026-05-08 13:33:15 +00:00

434 lines
14 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package otelcol handles the command-line, configuration, and runs the OpenTelemetry Collector.
// It contains the main [Collector] struct and its constructor [NewCollector].
// [Collector.Run] starts the Collector and then blocks until it shuts down.
package otelcol // import "go.opentelemetry.io/collector/otelcol"
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"sync"
"sync/atomic"
"go.uber.org/multierr"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/otelcol/internal/grpclog"
"go.opentelemetry.io/collector/service"
)
// State defines Collector's state.
type State int
const (
StateStarting State = iota
StateRunning
StateClosing
StateClosed
)
func (s State) String() string {
switch s {
case StateStarting:
return "Starting"
case StateRunning:
return "Running"
case StateClosing:
return "Closing"
case StateClosed:
return "Closed"
}
return "UNKNOWN"
}
// CollectorSettings holds configuration for creating a new Collector.
type CollectorSettings struct {
// Factories returns component factories for the collector.
//
// TODO(13263) This is a dangerous "bare" function value, should define an interface
// following style guidelines.
Factories func() (Factories, error)
// BuildInfo provides collector start information.
BuildInfo component.BuildInfo
// DisableGracefulShutdown disables the automatic graceful shutdown
// of the collector on SIGINT or SIGTERM.
// Users who want to handle signals themselves can disable this behavior
// and manually handle the signals to shutdown the collector.
DisableGracefulShutdown bool
// ConfigProviderSettings allows configuring the way the Collector retrieves its configuration
// The Collector will reload based on configuration changes from the ConfigProvider if any
// confmap.Providers watch for configuration changes.
ConfigProviderSettings ConfigProviderSettings
// ProviderModules maps provider schemes to their respective go modules.
ProviderModules map[string]string
// ConverterModules maps converter names to their respective go modules.
ConverterModules []string
// LoggingOptions provides a way to change behavior of zap logging.
LoggingOptions []zap.Option
// SkipSettingGRPCLogger avoids setting the grpc logger
SkipSettingGRPCLogger bool
}
// (Internal note) Collector Lifecycle:
// - New constructs a new Collector.
// - Run starts the collector.
// - Run calls setupConfigurationComponents to handle configuration.
// If configuration parser fails, collector's config can be reloaded.
// Collector can be shutdown if parser gets a shutdown error.
// - Run runs runAndWaitForShutdownEvent and waits for a shutdown event.
// SIGINT and SIGTERM, errors, and (*Collector).Shutdown can trigger the shutdown events.
// - Upon shutdown, pipelines are notified, then pipelines and extensions are shut down.
// - Users can call (*Collector).Shutdown anytime to shut down the collector.
// Collector represents a server providing the OpenTelemetry Collector service.
type Collector struct {
set CollectorSettings
buildZapLogger func(zap.Config, ...zap.Option) (*zap.Logger, error)
configProvider *ConfigProvider
service *service.Service
state *atomic.Int64
// shutdownChan is used to terminate the collector.
shutdownChan chan struct{}
shutdownOnce sync.Once
// wg is used by Shutdown to wait for Run to complete all cleanup.
wg sync.WaitGroup
// signalsChannel is used to receive termination signals from the OS.
signalsChannel chan os.Signal
// asyncErrorChannel is used to signal a fatal error from any component.
asyncErrorChannel chan error
bc *bufferedCore
updateConfigProviderLogger func(core zapcore.Core)
}
// NewCollector creates and returns a new instance of Collector.
func NewCollector(set CollectorSettings) (*Collector, error) {
bc := newBufferedCore(zapcore.DebugLevel)
cc := newCollectorCore(bc)
options := append([]zap.Option{zap.WithCaller(true)}, set.LoggingOptions...)
logger := zap.New(cc, options...)
set.ConfigProviderSettings.ResolverSettings.ProviderSettings = confmap.ProviderSettings{Logger: logger}
set.ConfigProviderSettings.ResolverSettings.ConverterSettings = confmap.ConverterSettings{Logger: logger}
configProvider, err := NewConfigProvider(set.ConfigProviderSettings)
if err != nil {
return nil, err
}
state := new(atomic.Int64)
state.Store(int64(StateStarting))
return &Collector{
set: set,
buildZapLogger: zap.Config.Build,
state: state,
shutdownChan: make(chan struct{}),
// Per signal.Notify documentation, a size of the channel equaled with
// the number of signals getting notified on is recommended.
signalsChannel: make(chan os.Signal, 3),
asyncErrorChannel: make(chan error),
configProvider: configProvider,
bc: bc,
updateConfigProviderLogger: cc.SetCore,
}, nil
}
// GetState returns current state of the collector server.
func (col *Collector) GetState() State {
return State(col.state.Load())
}
// Shutdown shuts down the collector server.
// If Run has been called, Shutdown blocks until Run completes all cleanup.
func (col *Collector) Shutdown() {
col.shutdownOnce.Do(func() {
close(col.shutdownChan)
})
col.wg.Wait()
}
func buildModuleInfo(m map[component.Type]string) map[component.Type]service.ModuleInfo {
moduleInfo := make(map[component.Type]service.ModuleInfo)
for k, v := range m {
moduleInfo[k] = service.ModuleInfo{BuilderRef: v}
}
return moduleInfo
}
// setupConfigurationComponents loads the config, creates the graph, and starts the components. If all the steps succeeds it
// sets the col.service with the service currently running.
func (col *Collector) setupConfigurationComponents(ctx context.Context) error {
col.setCollectorState(StateStarting)
factories, err := col.set.Factories()
if err != nil {
return fmt.Errorf("failed to initialize factories: %w", err)
}
cfg, err := col.configProvider.Get(ctx, factories)
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
if err = confmap.Validate(cfg); err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}
conf := confmap.New()
if err = conf.Marshal(cfg); err != nil {
return fmt.Errorf("could not marshal configuration: %w", err)
}
// Wrap the buildZapLogger to append LoggingOptions from collector settings,
// since service.Settings.LoggingOptions is deprecated.
buildZapLogger := col.buildZapLogger
if len(col.set.LoggingOptions) > 0 {
origBuildZapLogger := buildZapLogger
buildZapLogger = func(zapCfg zap.Config, opts ...zap.Option) (*zap.Logger, error) {
opts = append(opts, col.set.LoggingOptions...)
return origBuildZapLogger(zapCfg, opts...)
}
}
col.service, err = service.New(ctx, service.Settings{
BuildInfo: col.set.BuildInfo,
CollectorConf: conf,
ReceiversConfigs: cfg.Receivers,
ReceiversFactories: factories.Receivers,
ProcessorsConfigs: cfg.Processors,
ProcessorsFactories: factories.Processors,
ExportersConfigs: cfg.Exporters,
ExportersFactories: factories.Exporters,
ConnectorsConfigs: cfg.Connectors,
ConnectorsFactories: factories.Connectors,
ExtensionsConfigs: cfg.Extensions,
ExtensionsFactories: factories.Extensions,
ModuleInfos: service.ModuleInfos{
Receiver: buildModuleInfo(factories.ReceiverModules),
Processor: buildModuleInfo(factories.ProcessorModules),
Exporter: buildModuleInfo(factories.ExporterModules),
Extension: buildModuleInfo(factories.ExtensionModules),
Connector: buildModuleInfo(factories.ConnectorModules),
},
AsyncErrorChannel: col.asyncErrorChannel,
BuildZapLogger: buildZapLogger,
TelemetryFactory: factories.Telemetry,
}, cfg.Service)
if err != nil {
return err
}
if col.updateConfigProviderLogger != nil {
col.updateConfigProviderLogger(col.service.Logger().Core())
}
if col.bc != nil {
x := col.bc.TakeLogs()
for _, log := range x {
ce := col.service.Logger().Core().Check(log.Entry, nil)
if ce != nil {
ce.Write(log.Context...)
}
}
}
if !col.set.SkipSettingGRPCLogger {
grpclog.SetLogger(col.service.Logger())
}
if err = col.service.Start(ctx); err != nil {
return multierr.Combine(err, col.service.Shutdown(ctx))
}
col.setCollectorState(StateRunning)
return nil
}
func (col *Collector) reloadConfiguration(ctx context.Context) error {
col.service.Logger().Warn("Config updated, restart service")
col.setCollectorState(StateClosing)
if err := col.service.Shutdown(ctx); err != nil {
return fmt.Errorf("failed to shutdown the retiring config: %w", err)
}
if err := col.setupConfigurationComponents(ctx); err != nil {
return fmt.Errorf("failed to setup configuration components: %w", err)
}
return nil
}
func (col *Collector) DryRun(ctx context.Context) error {
factories, err := col.set.Factories()
if err != nil {
return fmt.Errorf("failed to initialize factories: %w", err)
}
cfg, err := col.configProvider.Get(ctx, factories)
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
if err := confmap.Validate(cfg); err != nil {
return err
}
return service.Validate(ctx, service.Settings{
BuildInfo: col.set.BuildInfo,
ReceiversConfigs: cfg.Receivers,
ReceiversFactories: factories.Receivers,
ProcessorsConfigs: cfg.Processors,
ProcessorsFactories: factories.Processors,
ExportersConfigs: cfg.Exporters,
ExportersFactories: factories.Exporters,
ConnectorsConfigs: cfg.Connectors,
ConnectorsFactories: factories.Connectors,
TelemetryFactory: factories.Telemetry,
}, service.Config{
Pipelines: cfg.Service.Pipelines,
})
}
func newFallbackLogger(options []zap.Option) (*zap.Logger, error) {
ec := zap.NewProductionEncoderConfig()
ec.EncodeTime = zapcore.ISO8601TimeEncoder
zapCfg := &zap.Config{
Level: zap.NewAtomicLevelAt(zapcore.DebugLevel),
Encoding: "console",
EncoderConfig: ec,
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
return zapCfg.Build(options...)
}
// Run starts the collector according to the given configuration, and waits for it to complete.
// Consecutive calls to Run are not allowed, Run shouldn't be called once a collector is shut down.
// Sets up the control logic for config reloading and shutdown.
// If Shutdown was called before Run, Run returns nil after cleaning up resources.
func (col *Collector) Run(ctx context.Context) error {
col.wg.Add(1)
defer col.wg.Done()
// If Shutdown was already called, return immediately without starting the service.
select {
case <-col.shutdownChan:
col.setCollectorState(StateClosed)
if err := col.configProvider.Shutdown(ctx); err != nil {
return fmt.Errorf("failed to shutdown config provider: %w", err)
}
return nil
default:
}
// setupConfigurationComponents is the "main" function responsible for startup
if err := col.setupConfigurationComponents(ctx); err != nil {
col.setCollectorState(StateClosed)
logger, loggerErr := newFallbackLogger(col.set.LoggingOptions)
if loggerErr != nil {
return errors.Join(err, fmt.Errorf("unable to create fallback logger: %w", loggerErr))
}
if col.bc != nil {
x := col.bc.TakeLogs()
for _, log := range x {
ce := logger.Core().Check(log.Entry, nil)
if ce != nil {
ce.Write(log.Context...)
}
}
}
return err
}
// Always notify with SIGHUP for configuration reloading.
signal.Notify(col.signalsChannel, SIGHUP)
defer signal.Stop(col.signalsChannel)
// Only notify with SIGTERM and SIGINT if graceful shutdown is enabled.
if !col.set.DisableGracefulShutdown {
signal.Notify(col.signalsChannel, os.Interrupt, SIGTERM)
}
// Control loop: selects between channels for various interrupts - when this loop is broken, the collector exits.
// If a configuration reload fails, we return without waiting for graceful shutdown.
LOOP:
for {
select {
case err := <-col.configProvider.Watch():
if err != nil {
col.service.Logger().Error("Config watch failed", zap.Error(err))
break LOOP
}
if err := col.reloadConfiguration(ctx); err != nil {
return err
}
case err := <-col.asyncErrorChannel:
col.service.Logger().Error("Asynchronous error received, terminating process", zap.Error(err))
break LOOP
case s := <-col.signalsChannel:
col.service.Logger().Info("Received signal from OS", zap.String("signal", s.String()))
if s != SIGHUP {
break LOOP
}
if err := col.reloadConfiguration(ctx); err != nil {
return err
}
case <-col.shutdownChan:
col.service.Logger().Info("Received shutdown request")
break LOOP
case <-ctx.Done():
col.service.Logger().Info("Context done, terminating process", zap.Error(ctx.Err()))
// Call shutdown with background context as the passed in context has been canceled
return col.shutdown(context.Background()) //nolint:contextcheck
}
}
return col.shutdown(ctx)
}
func (col *Collector) shutdown(ctx context.Context) error {
col.setCollectorState(StateClosing)
// Accumulate errors and proceed with shutting down remaining components.
var errs error
if err := col.configProvider.Shutdown(ctx); err != nil {
errs = multierr.Append(errs, fmt.Errorf("failed to shutdown config provider: %w", err))
}
// shutdown service
if err := col.service.Shutdown(ctx); err != nil {
errs = multierr.Append(errs, fmt.Errorf("failed to shutdown service after error: %w", err))
}
col.setCollectorState(StateClosed)
return errs
}
// setCollectorState provides current state of the collector
func (col *Collector) setCollectorState(state State) {
col.state.Store(int64(state))
}