diff --git a/changes/unreleased/Added-20260303-092813.yaml b/changes/unreleased/Added-20260303-092813.yaml new file mode 100644 index 00000000..a6221250 --- /dev/null +++ b/changes/unreleased/Added-20260303-092813.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Added ability to configure per-component log levels. +time: 2026-03-03T09:28:13.225331-05:00 diff --git a/docs/installation/configuration.md b/docs/installation/configuration.md index 8ebe0356..c06f7ac0 100644 --- a/docs/installation/configuration.md +++ b/docs/installation/configuration.md @@ -12,37 +12,52 @@ This reference uses a JSON-path like syntax to denote a nested property; for exa - [Configuration Reference](#configuration-reference) - [Required Settings](#required-settings) - [Optional Settings](#optional-settings) + - [Components](#components) ## Required Settings -| Property | Environment variable | Type | Description | Constraints | +| Property | Environment variable | Type | Description | Constraints | | :--------- | :------------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `host_id` | `PGEDGE_HOST_ID` | string | A logical identifier for the host that the Control Plane server is running on. This ID must be stable and unique to each Control Plane server instance. | Must be 1-63 characters long and contain only lower-cased letters, numbers, and hyphens (`-`). It must also start and end with either a lower-cased letter or number. | | `data_dir` | `PGEDGE_DATA_DIR` | string | A directory path where the Control Plane application data will be stored. This includes the server's internal database and configuration files as well as the data and configuration files for each Postgres database instance managed by this Control Plane server. | | ## Optional Settings -| Property | Environment variable | Type | Default | Description | Constraints | -| :------------------------------------------- | :--------------------------------------------------- | :------ | :--------------------- | :---------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- | -| `ipv4_address` | `PGEDGE_IPV4_ADDRESS` | string | Automatically detected | Can be used to override the automatically detected IP address of the host that runs this Control Plane server. | Must be a valid, stable IPv4 Address. | -| `hostname` | `PGEDGE_HOSTNAME` | string | Automatically detected | Can be used to override the automatically detected hostname of the host that runs this Control Plane server. | Must be a valid, stable hostname. | -| `stop_grace_period_seconds` | `PGEDGE_STOP_GRACE_PERIOD_SECONDS` | int | `30` | Controls the graceful shutdown period of the Control Plane server. | | -| `etcd_mode` | `PGEDGE_ETCD_MODE` | string | `server` | Determines whether this Control Plane server acts as an Etcd server or as a client only. | Must be one of: `server` or `client`. | -| `mqtt.enabled` | `PGEDGE_MQTT__ENABLED` | boolean | `false` | Exposes the Control Plane API over MQTT in addition to the default HTTP server. | | -| `mqtt.broker_url` | `PGEDGE_MQTT__BROKER_URL` | string | | URL to an MQTT broker for the MQTT integration. | | -| `mqtt.topic` | `PGEDGE_MQTT__TOPIC` | string | | The MQTT topic that the Control Plane server will subscribe to. | | -| `mqtt.client_id` | `PGEDGE_MQTT__CLIENT_ID` | string | | The client ID that the Control Plane uses when connecting to the MQTT broker. | | -| `mqtt.username` | `PGEDGE_MQTT__USERNAME` | string | | The username that the Control Plane uses when connecting to the MQTT broker. | | -| `mqtt.password` | `PGEDGE_MQTT__PASSWORD` | string | | The password that the Control Plane uses when connecting to the MQTT broker. | | -| `http.bind_addr` | `PGEDGE_HTTP__BIND_ADDR` | string | `0.0.0.0` | The address that the Control Plane HTTP server will listen on. Defaults to `0.0.0.0` to listen on all interfaces. | Must be accessible by other Control Plane server instances in this cluster. | -| `http.port` | `PGEDGE_HTTP__PORT` | int | `3000` | The port that the Control Plane HTTP server will listen on. | | -| `logging.level` | `PGEDGE_LOGGING__LEVEL` | string | `info` | The log level for the Control Plane server. | | -| `logging.pretty` | `PGEDGE_LOGGING__PRETTY` | boolean | `false` | Enables human-readable logs and colorization. | | -| `etcd_server.log_level` | `PGEDGE_ETCD_SERVER__LOG_LEVEL` | string | `fatal` | The log level for the embedded Etcd server. | | -| `etcd_server.peer_port` | `PGEDGE_ETCD_SERVER__PEER_PORT` | int | `2380` | The port that the embedded Etcd server will listen on for peer connections. | | -| `etcd_server.client_port` | `PGEDGE_ETCD_SERVER__CLIENT_PORT` | int | `2379` | The port that the embedded Etcd server will listen on for client connections. | | -| `etcd_client.log_level` | `PGEDGE_ETCD_CLIENT__LOG_LEVEL` | string | `fatal` | The log level for Etcd client operations performed by this Control Plane server. | | -| `docker_swarm.image_repository_host` | `PGEDGE_DOCKER_SWARM__IMAGE_REPOSITORY_HOST` | string | `ghcr.io/pgedge` | The base URL of pgEdge Docker images. | | -| `docker_swarm.database_networks_cidr` | `PGEDGE_DOCKER_SWARM__DATABASE_NETWORKS_CIDR` | string | `10.128.128.0/18` | The CIDR used to allocate per-database networks. | Must not be changed after creating databases. | -| `docker_swarm.database_networks_subnet_bits` | `PGEDGE_DOCKER_SWARM__DATABASE_NETWORKS_SUBNET_BITS` | int | `26` | The subnet size for per-database networks. | Must not be changed after creating databases. | -| `database_owner_uid` | `PGEDGE_DATABASE_OWNER_UID` | int | `26` | The UID to use for database configuration and data. | Must match the UID that owns the Postgres server processes. | +| Property | Environment variable | Type | Default | Description | Constraints | +| :------------------------------------------- | :--------------------------------------------------- | :------ | :--------------------- | :---------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | +| `ipv4_address` | `PGEDGE_IPV4_ADDRESS` | string | Automatically detected | Can be used to override the automatically detected IP address of the host that runs this Control Plane server. | Must be a valid, stable IPv4 Address. | +| `hostname` | `PGEDGE_HOSTNAME` | string | Automatically detected | Can be used to override the automatically detected hostname of the host that runs this Control Plane server. | Must be a valid, stable hostname. | +| `stop_grace_period_seconds` | `PGEDGE_STOP_GRACE_PERIOD_SECONDS` | int | `30` | Controls the graceful shutdown period of the Control Plane server. | | +| `etcd_mode` | `PGEDGE_ETCD_MODE` | string | `server` | Determines whether this Control Plane server acts as an Etcd server or as a client only. | Must be one of: `server` or `client`. | +| `mqtt.enabled` | `PGEDGE_MQTT__ENABLED` | boolean | `false` | Exposes the Control Plane API over MQTT in addition to the default HTTP server. | | +| `mqtt.broker_url` | `PGEDGE_MQTT__BROKER_URL` | string | | URL to an MQTT broker for the MQTT integration. | | +| `mqtt.topic` | `PGEDGE_MQTT__TOPIC` | string | | The MQTT topic that the Control Plane server will subscribe to. | | +| `mqtt.client_id` | `PGEDGE_MQTT__CLIENT_ID` | string | | The client ID that the Control Plane uses when connecting to the MQTT broker. | | +| `mqtt.username` | `PGEDGE_MQTT__USERNAME` | string | | The username that the Control Plane uses when connecting to the MQTT broker. | | +| `mqtt.password` | `PGEDGE_MQTT__PASSWORD` | string | | The password that the Control Plane uses when connecting to the MQTT broker. | | +| `http.bind_addr` | `PGEDGE_HTTP__BIND_ADDR` | string | `0.0.0.0` | The address that the Control Plane HTTP server will listen on. Defaults to `0.0.0.0` to listen on all interfaces. | Must be accessible by other Control Plane server instances in this cluster. | +| `http.port` | `PGEDGE_HTTP__PORT` | int | `3000` | The port that the Control Plane HTTP server will listen on. | | +| `logging.level` | `PGEDGE_LOGGING__LEVEL` | string | `info` | The log level for the Control Plane server. | Must be one of: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or `panic` | +| `logging.pretty` | `PGEDGE_LOGGING__PRETTY` | boolean | `false` | Enables human-readable logs and colorization. | | +| `logging.component_levels.` | `PGEDGE_LOGGING__COMPONENT_LEVELS_` | string | | Enables you to set per-component log levels. The list of components are available in [Components](#components). | Must be one of: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or `panic` | +| `etcd_server.log_level` | `PGEDGE_ETCD_SERVER__LOG_LEVEL` | string | `fatal` | The log level for the embedded Etcd server. | | +| `etcd_server.peer_port` | `PGEDGE_ETCD_SERVER__PEER_PORT` | int | `2380` | The port that the embedded Etcd server will listen on for peer connections. | | +| `etcd_server.client_port` | `PGEDGE_ETCD_SERVER__CLIENT_PORT` | int | `2379` | The port that the embedded Etcd server will listen on for client connections. | | +| `etcd_client.log_level` | `PGEDGE_ETCD_CLIENT__LOG_LEVEL` | string | `fatal` | The log level for Etcd client operations performed by this Control Plane server. | | +| `docker_swarm.image_repository_host` | `PGEDGE_DOCKER_SWARM__IMAGE_REPOSITORY_HOST` | string | `ghcr.io/pgedge` | The base URL of pgEdge Docker images. | | +| `docker_swarm.database_networks_cidr` | `PGEDGE_DOCKER_SWARM__DATABASE_NETWORKS_CIDR` | string | `10.128.128.0/18` | The CIDR used to allocate per-database networks. | Must not be changed after creating databases. | +| `docker_swarm.database_networks_subnet_bits` | `PGEDGE_DOCKER_SWARM__DATABASE_NETWORKS_SUBNET_BITS` | int | `26` | The subnet size for per-database networks. | Must not be changed after creating databases. | +| `database_owner_uid` | `PGEDGE_DATABASE_OWNER_UID` | int | `26` | The UID to use for database configuration and data. | Must match the UID that owns the Postgres server processes. | + +### Components + +This is the current list of components that can be configured in the `logging.component_levels` setting: + +- `api_server` +- `election_candidate` +- `embedded_etcd` +- `remote_etcd` +- `migration` +- `migration_runner` +- `scheduler_service` +- `workflows_worker` diff --git a/server/internal/api/provide.go b/server/internal/api/provide.go index cf6d28c6..9e8daa22 100644 --- a/server/internal/api/provide.go +++ b/server/internal/api/provide.go @@ -3,11 +3,11 @@ package api import ( "fmt" - "github.com/rs/zerolog" "github.com/samber/do" "github.com/pgEdge/control-plane/server/internal/api/apiv1" "github.com/pgEdge/control-plane/server/internal/config" + "github.com/pgEdge/control-plane/server/internal/logging" ) func Provide(i *do.Injector) { @@ -21,14 +21,14 @@ func provideServer(i *do.Injector) { if err != nil { return nil, fmt.Errorf("failed to get config: %w", err) } - logger, err := do.Invoke[zerolog.Logger](i) + loggerFactory, err := do.Invoke[*logging.Factory](i) if err != nil { - return nil, fmt.Errorf("failed to get logger: %w", err) + return nil, fmt.Errorf("failed to get logger factory: %w", err) } v1Svc, err := do.Invoke[*apiv1.Service](i) if err != nil { return nil, fmt.Errorf("failed to get v1 api service: %w", err) } - return NewServer(cfg, logger, v1Svc), nil + return NewServer(cfg, loggerFactory, v1Svc), nil }) } diff --git a/server/internal/api/server.go b/server/internal/api/server.go index c89eaed2..539d6618 100644 --- a/server/internal/api/server.go +++ b/server/internal/api/server.go @@ -12,6 +12,7 @@ import ( "github.com/pgEdge/control-plane/server/internal/api/apiv1" "github.com/pgEdge/control-plane/server/internal/config" + "github.com/pgEdge/control-plane/server/internal/logging" ) var _ do.Shutdownable = (*Server)(nil) @@ -28,7 +29,7 @@ type Server struct { func NewServer( cfg config.Config, - logger zerolog.Logger, + loggerFactory *logging.Factory, v1Svc *apiv1.Service, ) *Server { mux := goahttp.NewMuxer() @@ -45,10 +46,7 @@ func NewServer( // Mount all the v1 handlers v1Svc.Mount(mux) - logger = logger.With(). - Str("component", "api_server"). - Logger() - + logger := loggerFactory.Logger("api_server") handler := addMiddleware(logger, mux) var ( diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 48dc70a2..5a183a3a 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -34,8 +34,9 @@ func validateOptionalID(name, value string) error { } type Logging struct { - Level string `koanf:"level" json:"level,omitempty"` - Pretty bool `koanf:"pretty" json:"pretty,omitempty"` + Level string `koanf:"level" json:"level,omitempty"` + Pretty bool `koanf:"pretty" json:"pretty,omitempty"` + ComponentLevels map[string]string `koanf:"component_levels" json:"component_levels,omitempty"` } func (l Logging) validate() []error { diff --git a/server/internal/election/candidate.go b/server/internal/election/candidate.go index 2d2992c4..2ec834a6 100644 --- a/server/internal/election/candidate.go +++ b/server/internal/election/candidate.go @@ -8,6 +8,7 @@ import ( "sync/atomic" "time" + "github.com/pgEdge/control-plane/server/internal/logging" "github.com/pgEdge/control-plane/server/internal/storage" "github.com/rs/zerolog" ) @@ -42,7 +43,7 @@ type Candidate struct { // leadership. func NewCandidate( store *ElectionStore, - logger zerolog.Logger, + loggerFactory *logging.Factory, electionName Name, candidateID string, ttl time.Duration, @@ -50,8 +51,7 @@ func NewCandidate( ) *Candidate { return &Candidate{ store: store, - logger: logger.With(). - Str("component", "election_candidate"). + logger: loggerFactory.Logger("election_candidate").With(). Stringer("election_name", electionName). Str("candidate_id", candidateID). Logger(), diff --git a/server/internal/election/candidate_test.go b/server/internal/election/candidate_test.go index 734d3d29..44ef48e5 100644 --- a/server/internal/election/candidate_test.go +++ b/server/internal/election/candidate_test.go @@ -16,9 +16,9 @@ import ( func TestCandidate(t *testing.T) { server := storagetest.NewEtcdTestServer(t) client := server.Client(t) - logger := testutils.Logger(t) + loggerFactory := testutils.LoggerFactory(t) store := election.NewElectionStore(client, uuid.NewString()) - electionSvc := election.NewService(store, logger) + electionSvc := election.NewService(store, loggerFactory) t.Run("basic functionality", func(t *testing.T) { ctx := t.Context() diff --git a/server/internal/election/provide.go b/server/internal/election/provide.go index 5e3a5f74..2448e1f3 100644 --- a/server/internal/election/provide.go +++ b/server/internal/election/provide.go @@ -1,10 +1,11 @@ package election import ( - "github.com/pgEdge/control-plane/server/internal/config" - "github.com/rs/zerolog" "github.com/samber/do" clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/pgEdge/control-plane/server/internal/config" + "github.com/pgEdge/control-plane/server/internal/logging" ) func Provide(i *do.Injector) { @@ -32,10 +33,10 @@ func provideService(i *do.Injector) { if err != nil { return nil, err } - logger, err := do.Invoke[zerolog.Logger](i) + loggerFactory, err := do.Invoke[*logging.Factory](i) if err != nil { return nil, err } - return NewService(store, logger), nil + return NewService(store, loggerFactory), nil }) } diff --git a/server/internal/election/service.go b/server/internal/election/service.go index 8c488a77..bef4113a 100644 --- a/server/internal/election/service.go +++ b/server/internal/election/service.go @@ -3,28 +3,28 @@ package election import ( "time" - "github.com/rs/zerolog" + "github.com/pgEdge/control-plane/server/internal/logging" ) // Service manages election operations. type Service struct { - store *ElectionStore - logger zerolog.Logger + store *ElectionStore + loggerFactory *logging.Factory } // NewService returns a new Service. func NewService( store *ElectionStore, - logger zerolog.Logger, + loggerFactory *logging.Factory, ) *Service { return &Service{ - store: store, - logger: logger, + store: store, + loggerFactory: loggerFactory, } } // NewCandidate creates a new Candidate for the given election. candidateID must // be unique amongst candidates. func (s *Service) NewCandidate(electionName Name, candidateID string, ttl time.Duration, onClaim ...ClaimHandler) *Candidate { - return NewCandidate(s.store, s.logger, electionName, candidateID, ttl, onClaim) + return NewCandidate(s.store, s.loggerFactory, electionName, candidateID, ttl, onClaim) } diff --git a/server/internal/etcd/embedded.go b/server/internal/etcd/embedded.go index 74a81430..75537f7c 100644 --- a/server/internal/etcd/embedded.go +++ b/server/internal/etcd/embedded.go @@ -22,6 +22,7 @@ import ( "github.com/pgEdge/control-plane/server/internal/certificates" "github.com/pgEdge/control-plane/server/internal/config" "github.com/pgEdge/control-plane/server/internal/healthcheck" + "github.com/pgEdge/control-plane/server/internal/logging" "github.com/pgEdge/control-plane/server/internal/utils" ) @@ -32,22 +33,22 @@ var _ do.Shutdownable = (*EmbeddedEtcd)(nil) const quotaBackendBytes = 8 * 1024 * 1024 * 1024 // 8GB type EmbeddedEtcd struct { - mu sync.Mutex - certSvc *certificates.Service - client *clientv3.Client - etcd *embed.Etcd - logger zerolog.Logger - cfg *config.Manager - initialized chan struct{} + mu sync.Mutex + certSvc *certificates.Service + client *clientv3.Client + etcd *embed.Etcd + logger zerolog.Logger + loggerFactory *logging.Factory + cfg *config.Manager + initialized chan struct{} } -func NewEmbeddedEtcd(cfg *config.Manager, logger zerolog.Logger) *EmbeddedEtcd { +func NewEmbeddedEtcd(cfg *config.Manager, loggerFactory *logging.Factory) *EmbeddedEtcd { return &EmbeddedEtcd{ - cfg: cfg, - initialized: make(chan struct{}), - logger: logger.With(). - Str("component", "etcd_server"). - Logger(), + cfg: cfg, + initialized: make(chan struct{}), + logger: loggerFactory.Logger("embedded_etcd"), + loggerFactory: loggerFactory, } } @@ -522,7 +523,7 @@ func (e *EmbeddedEtcd) ChangeMode(ctx context.Context, mode config.EtcdMode) (Et return nil, err } - remote := NewRemoteEtcd(e.cfg, e.logger) + remote := NewRemoteEtcd(e.cfg, e.loggerFactory) if err := remote.Start(ctx); err != nil { return nil, fmt.Errorf("failed to start remote client: %w", err) } diff --git a/server/internal/etcd/embedded_test.go b/server/internal/etcd/embedded_test.go index 67d0d3c8..0c334d43 100644 --- a/server/internal/etcd/embedded_test.go +++ b/server/internal/etcd/embedded_test.go @@ -38,7 +38,7 @@ func TestEmbeddedEtcd(t *testing.T) { }, } - server := etcd.NewEmbeddedEtcd(cfgMgr(t, cfg), testutils.Logger(t)) + server := etcd.NewEmbeddedEtcd(cfgMgr(t, cfg), testutils.LoggerFactory(t)) require.NotNil(t, server) initialized, err := server.IsInitialized() @@ -101,7 +101,7 @@ func TestEmbeddedEtcd(t *testing.T) { }, } - serverA := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgA), testutils.Logger(t)) + serverA := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgA), testutils.LoggerFactory(t)) require.NotNil(t, serverA) err := serverA.Start(ctx) @@ -130,7 +130,7 @@ func TestEmbeddedEtcd(t *testing.T) { }, } - serverB := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgB), testutils.Logger(t)) + serverB := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgB), testutils.LoggerFactory(t)) require.NotNil(t, serverB) // Generate credentials for server B @@ -209,7 +209,7 @@ func TestEmbeddedEtcd(t *testing.T) { }) t.Run("three member cluster", func(t *testing.T) { - logger := testutils.Logger(t) + loggerFactory := testutils.LoggerFactory(t) ctx := context.Background() // Initialize the cluster @@ -224,7 +224,7 @@ func TestEmbeddedEtcd(t *testing.T) { PeerPort: storagetest.GetFreePort(t), }, } - serverA := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgA), logger) + serverA := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgA), loggerFactory) require.NoError(t, serverA.Start(ctx)) t.Cleanup(func() { serverA.Shutdown() @@ -241,7 +241,7 @@ func TestEmbeddedEtcd(t *testing.T) { PeerPort: storagetest.GetFreePort(t), }, } - serverB := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgB), logger) + serverB := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgB), loggerFactory) cfgC := config.Config{ HostID: uuid.NewString(), @@ -254,7 +254,7 @@ func TestEmbeddedEtcd(t *testing.T) { PeerPort: storagetest.GetFreePort(t), }, } - serverC := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgC), logger) + serverC := etcd.NewEmbeddedEtcd(cfgMgr(t, cfgC), loggerFactory) leader, err := serverA.Leader(ctx) require.NoError(t, err) @@ -387,7 +387,7 @@ func TestEmbeddedEtcd(t *testing.T) { }, } - server := etcd.NewEmbeddedEtcd(cfgMgr(t, cfg), testutils.Logger(t)) + server := etcd.NewEmbeddedEtcd(cfgMgr(t, cfg), testutils.LoggerFactory(t)) require.NotNil(t, server) err := server.Start(ctx) diff --git a/server/internal/etcd/provide.go b/server/internal/etcd/provide.go index eed6271a..9e56e7b1 100644 --- a/server/internal/etcd/provide.go +++ b/server/internal/etcd/provide.go @@ -11,6 +11,7 @@ import ( "google.golang.org/grpc/grpclog" "github.com/pgEdge/control-plane/server/internal/config" + "github.com/pgEdge/control-plane/server/internal/logging" ) func Provide(i *do.Injector) { @@ -30,12 +31,12 @@ func provideClient(i *do.Injector) { } // newEtcdForMode creates an Etcd instance based on the specified mode. -func newEtcdForMode(mode config.EtcdMode, cfg *config.Manager, logger zerolog.Logger) (Etcd, error) { +func newEtcdForMode(mode config.EtcdMode, cfg *config.Manager, loggerFactory *logging.Factory) (Etcd, error) { switch mode { case config.EtcdModeServer: - return NewEmbeddedEtcd(cfg, logger), nil + return NewEmbeddedEtcd(cfg, loggerFactory), nil case config.EtcdModeClient: - return NewRemoteEtcd(cfg, logger), nil + return NewRemoteEtcd(cfg, loggerFactory), nil default: return nil, fmt.Errorf("invalid etcd mode: %s", mode) } @@ -51,6 +52,10 @@ func provideEtcd(i *do.Injector) { if err != nil { return nil, err } + loggerFactory, err := do.Invoke[*logging.Factory](i) + if err != nil { + return nil, err + } appCfg := cfg.Config() generated := cfg.GeneratedConfig() @@ -71,7 +76,7 @@ func provideEtcd(i *do.Injector) { switch { case oldMode == "" || oldMode == newMode: - etcd, err := newEtcdForMode(newMode, cfg, logger) + etcd, err := newEtcdForMode(newMode, cfg, loggerFactory) if err != nil { return nil, err } @@ -90,10 +95,10 @@ func provideEtcd(i *do.Injector) { return etcd, nil case oldMode == config.EtcdModeServer && newMode == config.EtcdModeClient: - embedded := NewEmbeddedEtcd(cfg, logger) + embedded := NewEmbeddedEtcd(cfg, loggerFactory) return embedded.ChangeMode(ctx, newMode) case oldMode == config.EtcdModeClient && newMode == config.EtcdModeServer: - remote := NewRemoteEtcd(cfg, logger) + remote := NewRemoteEtcd(cfg, loggerFactory) return remote.ChangeMode(ctx, newMode) default: return nil, fmt.Errorf("unsupported etcd mode transition: %s -> %s", oldMode, newMode) diff --git a/server/internal/etcd/remote.go b/server/internal/etcd/remote.go index 36abe393..bb4cf408 100644 --- a/server/internal/etcd/remote.go +++ b/server/internal/etcd/remote.go @@ -14,6 +14,7 @@ import ( "github.com/pgEdge/control-plane/server/internal/certificates" "github.com/pgEdge/control-plane/server/internal/config" "github.com/pgEdge/control-plane/server/internal/healthcheck" + "github.com/pgEdge/control-plane/server/internal/logging" ) var ErrOperationNotSupported = errors.New("operation not supported") @@ -22,23 +23,23 @@ var _ Etcd = (*RemoteEtcd)(nil) var _ do.Shutdownable = (*RemoteEtcd)(nil) type RemoteEtcd struct { - mu sync.Mutex - certSvc *certificates.Service - client *clientv3.Client - logger zerolog.Logger - cfg *config.Manager - initialized chan struct{} - err chan error + mu sync.Mutex + certSvc *certificates.Service + client *clientv3.Client + logger zerolog.Logger + loggerFactory *logging.Factory + cfg *config.Manager + initialized chan struct{} + err chan error } -func NewRemoteEtcd(cfg *config.Manager, logger zerolog.Logger) *RemoteEtcd { +func NewRemoteEtcd(cfg *config.Manager, loggerFactory *logging.Factory) *RemoteEtcd { return &RemoteEtcd{ - cfg: cfg, - initialized: make(chan struct{}), - err: make(chan error), - logger: logger.With(). - Str("component", "etcd_client"). - Logger(), + cfg: cfg, + initialized: make(chan struct{}), + err: make(chan error), + logger: loggerFactory.Logger("remote_etcd"), + loggerFactory: loggerFactory, } } @@ -299,7 +300,7 @@ func (r *RemoteEtcd) ChangeMode(ctx context.Context, mode config.EtcdMode) (Etcd return nil, err } - embedded := NewEmbeddedEtcd(r.cfg, r.logger) + embedded := NewEmbeddedEtcd(r.cfg, r.loggerFactory) err = embedded.Join(ctx, JoinOptions{ Leader: leader, Credentials: creds, diff --git a/server/internal/etcd/remote_test.go b/server/internal/etcd/remote_test.go index 0bdba1f4..7bc47ae5 100644 --- a/server/internal/etcd/remote_test.go +++ b/server/internal/etcd/remote_test.go @@ -30,7 +30,7 @@ func TestRemoteEtcd(t *testing.T) { LogLevel: "debug", }, } - remote := etcd.NewRemoteEtcd(cfgMgr(t, cfg), testutils.Logger(t)) + remote := etcd.NewRemoteEtcd(cfgMgr(t, cfg), testutils.LoggerFactory(t)) join(t, serverA, remote, cfg) @@ -99,7 +99,7 @@ func testEmbedded(t testing.TB) (*etcd.EmbeddedEtcd, config.Config) { PeerPort: storagetest.GetFreePort(t), }, } - server := etcd.NewEmbeddedEtcd(cfgMgr(t, cfg), testutils.Logger(t)) + server := etcd.NewEmbeddedEtcd(cfgMgr(t, cfg), testutils.LoggerFactory(t)) t.Cleanup(func() { server.Shutdown() diff --git a/server/internal/logging/factory.go b/server/internal/logging/factory.go new file mode 100644 index 00000000..cdcf6a0c --- /dev/null +++ b/server/internal/logging/factory.go @@ -0,0 +1,41 @@ +package logging + +import ( + "fmt" + + "github.com/rs/zerolog" + + "github.com/pgEdge/control-plane/server/internal/config" +) + +type Factory struct { + base zerolog.Logger + componentLevels map[string]zerolog.Level +} + +func NewFactory(cfg config.Config, base zerolog.Logger) (*Factory, error) { + componentLevels := map[string]zerolog.Level{} + + for component, l := range cfg.Logging.ComponentLevels { + level, err := zerolog.ParseLevel(l) + if err != nil { + return nil, fmt.Errorf("failed to parse level for component '%s': %w", component, err) + } + componentLevels[component] = level + } + + return &Factory{ + base: base, + componentLevels: componentLevels, + }, nil +} + +func (f *Factory) Logger(component string) zerolog.Logger { + logger := f.base + level, ok := f.componentLevels[component] + if ok { + logger = logger.Level(level) + } + + return logger.With().Str("component", component).Logger() +} diff --git a/server/internal/logging/provide.go b/server/internal/logging/provide.go index e3156378..baf8cf7d 100644 --- a/server/internal/logging/provide.go +++ b/server/internal/logging/provide.go @@ -11,6 +11,7 @@ import ( func Provide(i *do.Injector) { provideLogger(i) + provideFactory(i) } func provideLogger(i *do.Injector) { @@ -26,3 +27,17 @@ func provideLogger(i *do.Injector) { return logger, nil }) } + +func provideFactory(i *do.Injector) { + do.Provide(i, func(i *do.Injector) (*Factory, error) { + cfg, err := do.Invoke[config.Config](i) + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + logger, err := do.Invoke[zerolog.Logger](i) + if err != nil { + return nil, fmt.Errorf("failed to get base logger: %w", err) + } + return NewFactory(cfg, logger) + }) +} diff --git a/server/internal/migrate/migrations/add_task_scope.go b/server/internal/migrate/migrations/add_task_scope.go index e4ff9dd9..1340049e 100644 --- a/server/internal/migrate/migrations/add_task_scope.go +++ b/server/internal/migrate/migrations/add_task_scope.go @@ -7,12 +7,13 @@ import ( "time" "github.com/google/uuid" + "github.com/samber/do" + clientv3 "go.etcd.io/etcd/client/v3" + "github.com/pgEdge/control-plane/server/internal/config" + "github.com/pgEdge/control-plane/server/internal/logging" "github.com/pgEdge/control-plane/server/internal/storage" "github.com/pgEdge/control-plane/server/internal/task" - "github.com/rs/zerolog" - "github.com/samber/do" - clientv3 "go.etcd.io/etcd/client/v3" ) type AddTaskScope struct{} @@ -26,7 +27,7 @@ func (a *AddTaskScope) Run(ctx context.Context, i *do.Injector) error { if err != nil { return fmt.Errorf("failed to initialize config: %w", err) } - logger, err := do.Invoke[zerolog.Logger](i) + loggerFactory, err := do.Invoke[*logging.Factory](i) if err != nil { return fmt.Errorf("failed to initialize logger: %w", err) } @@ -39,8 +40,7 @@ func (a *AddTaskScope) Run(ctx context.Context, i *do.Injector) error { return fmt.Errorf("failed to initialize task store: %w", err) } - logger = logger.With(). - Str("component", "migration"). + logger := loggerFactory.Logger("migration").With(). Str("identifier", a.Identifier()). Logger() diff --git a/server/internal/migrate/provide.go b/server/internal/migrate/provide.go index 2d4ba81a..574accd2 100644 --- a/server/internal/migrate/provide.go +++ b/server/internal/migrate/provide.go @@ -3,12 +3,12 @@ package migrate import ( "time" - "github.com/rs/zerolog" "github.com/samber/do" clientv3 "go.etcd.io/etcd/client/v3" "github.com/pgEdge/control-plane/server/internal/config" "github.com/pgEdge/control-plane/server/internal/election" + "github.com/pgEdge/control-plane/server/internal/logging" ) const ElectionName = election.Name("migration_runner") @@ -44,7 +44,7 @@ func provideRunner(i *do.Injector) { if err != nil { return nil, err } - logger, err := do.Invoke[zerolog.Logger](i) + loggerFactory, err := do.Invoke[*logging.Factory](i) if err != nil { return nil, err } @@ -62,7 +62,7 @@ func provideRunner(i *do.Injector) { cfg.HostID, store, i, - logger, + loggerFactory, migrations, locker, ), nil diff --git a/server/internal/migrate/runner.go b/server/internal/migrate/runner.go index d7e46f10..a91f8cea 100644 --- a/server/internal/migrate/runner.go +++ b/server/internal/migrate/runner.go @@ -11,6 +11,7 @@ import ( "github.com/samber/do" "github.com/pgEdge/control-plane/server/internal/election" + "github.com/pgEdge/control-plane/server/internal/logging" "github.com/pgEdge/control-plane/server/internal/storage" "github.com/pgEdge/control-plane/server/internal/version" ) @@ -39,17 +40,15 @@ func NewRunner( hostID string, store *Store, injector *do.Injector, - logger zerolog.Logger, + loggerFactory *logging.Factory, migrations []Migration, candidate *election.Candidate, ) *Runner { return &Runner{ - hostID: hostID, - store: store, - injector: injector, - logger: logger.With(). - Str("component", "migration_runner"). - Logger(), + hostID: hostID, + store: store, + injector: injector, + logger: loggerFactory.Logger("migration_runner"), migrations: migrations, candidate: candidate, errCh: make(chan error, 1), diff --git a/server/internal/migrate/runner_test.go b/server/internal/migrate/runner_test.go index 603a7419..945a27f8 100644 --- a/server/internal/migrate/runner_test.go +++ b/server/internal/migrate/runner_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/google/uuid" - "github.com/rs/zerolog" "github.com/samber/do" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -23,11 +22,11 @@ import ( func TestRunner(t *testing.T) { server := storagetest.NewEtcdTestServer(t) client := server.Client(t) - logger := testutils.Logger(t) + loggerFactory := testutils.LoggerFactory(t) t.Run("acquires lock and runs migrations", func(t *testing.T) { root := uuid.NewString() - electionSvc := election.NewService(election.NewElectionStore(client, root), logger) + electionSvc := election.NewService(election.NewElectionStore(client, root), loggerFactory) store := migrate.NewStore(client, root) i := do.New() @@ -41,7 +40,7 @@ func TestRunner(t *testing.T) { } candidate := testCandidate(t, electionSvc, "host-1") - runner := migrate.NewRunner("host-1", store, i, logger, []migrate.Migration{m}, candidate) + runner := migrate.NewRunner("host-1", store, i, loggerFactory, []migrate.Migration{m}, candidate) err := runner.Run(t.Context()) require.NoError(t, err) assert.True(t, ran, "migration should have run") @@ -52,7 +51,7 @@ func TestRunner(t *testing.T) { // successfully and that the migration is only run once. root := uuid.NewString() - electionSvc := election.NewService(election.NewElectionStore(client, root), logger) + electionSvc := election.NewService(election.NewElectionStore(client, root), loggerFactory) store := migrate.NewStore(client, root) i := do.New() @@ -77,7 +76,7 @@ func TestRunner(t *testing.T) { defer wg.Done() candidate := testCandidate(t, electionSvc, "host-1") - runner := migrate.NewRunner("host-1", store, i, logger, []migrate.Migration{m}, candidate) + runner := migrate.NewRunner("host-1", store, i, loggerFactory, []migrate.Migration{m}, candidate) require.NoError(t, runner.Run(t.Context())) }() @@ -86,7 +85,7 @@ func TestRunner(t *testing.T) { defer wg.Done() candidate := testCandidate(t, electionSvc, "host-2") - runner := migrate.NewRunner("host-2", store, i, logger, []migrate.Migration{m}, candidate) + runner := migrate.NewRunner("host-2", store, i, loggerFactory, []migrate.Migration{m}, candidate) require.NoError(t, runner.Run(t.Context())) }() @@ -100,11 +99,11 @@ func TestRunner(t *testing.T) { func TestRunnerMigrationOrdering(t *testing.T) { server := storagetest.NewEtcdTestServer(t) client := server.Client(t) - logger := zerolog.Nop() + loggerFactory := testutils.LoggerFactory(t) t.Run("runs migrations in order", func(t *testing.T) { root := uuid.NewString() - electionSvc := election.NewService(election.NewElectionStore(client, root), logger) + electionSvc := election.NewService(election.NewElectionStore(client, root), loggerFactory) store := migrate.NewStore(client, root) i := do.New() @@ -132,7 +131,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { } candidate := testCandidate(t, electionSvc, "host-1") - runner := migrate.NewRunner("host-1", store, i, logger, []migrate.Migration{m1, m2, m3}, candidate) + runner := migrate.NewRunner("host-1", store, i, loggerFactory, []migrate.Migration{m1, m2, m3}, candidate) err := runner.Run(t.Context()) require.NoError(t, err) @@ -141,7 +140,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { t.Run("starts from current revision", func(t *testing.T) { root := uuid.NewString() - electionSvc := election.NewService(election.NewElectionStore(client, root), logger) + electionSvc := election.NewService(election.NewElectionStore(client, root), loggerFactory) store := migrate.NewStore(client, root) i := do.New() @@ -173,7 +172,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { } candidate := testCandidate(t, electionSvc, "host-1") - runner := migrate.NewRunner("host-1", store, i, logger, []migrate.Migration{m1, m2, m3}, candidate) + runner := migrate.NewRunner("host-1", store, i, loggerFactory, []migrate.Migration{m1, m2, m3}, candidate) err = runner.Run(t.Context()) require.NoError(t, err) @@ -183,7 +182,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { t.Run("stops on first failure", func(t *testing.T) { root := uuid.NewString() - electionSvc := election.NewService(election.NewElectionStore(client, root), logger) + electionSvc := election.NewService(election.NewElectionStore(client, root), loggerFactory) store := migrate.NewStore(client, root) i := do.New() @@ -211,7 +210,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { } candidate := testCandidate(t, electionSvc, "host-1") - runner := migrate.NewRunner("host-1", store, i, logger, []migrate.Migration{m1, m2, m3}, candidate) + runner := migrate.NewRunner("host-1", store, i, loggerFactory, []migrate.Migration{m1, m2, m3}, candidate) err := runner.Run(t.Context()) assert.ErrorContains(t, err, "migration failed") @@ -221,7 +220,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { t.Run("records status for each migration", func(t *testing.T) { root := uuid.NewString() - electionSvc := election.NewService(election.NewElectionStore(client, root), logger) + electionSvc := election.NewService(election.NewElectionStore(client, root), loggerFactory) store := migrate.NewStore(client, root) i := do.New() @@ -229,7 +228,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { m2 := &runnerMockMigration{id: "migration-2", err: errors.New("failed")} candidate := testCandidate(t, electionSvc, "host-1") - runner := migrate.NewRunner("host-1", store, i, logger, []migrate.Migration{m1, m2}, candidate) + runner := migrate.NewRunner("host-1", store, i, loggerFactory, []migrate.Migration{m1, m2}, candidate) err := runner.Run(t.Context()) assert.ErrorContains(t, err, "failed") @@ -253,7 +252,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { t.Run("updates revision after each successful migration", func(t *testing.T) { root := uuid.NewString() - electionSvc := election.NewService(election.NewElectionStore(client, root), logger) + electionSvc := election.NewService(election.NewElectionStore(client, root), loggerFactory) store := migrate.NewStore(client, root) i := do.New() @@ -261,7 +260,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { m2 := &runnerMockMigration{id: "migration-2"} candidate := testCandidate(t, electionSvc, "host-1") - runner := migrate.NewRunner("host-1", store, i, logger, []migrate.Migration{m1, m2}, candidate) + runner := migrate.NewRunner("host-1", store, i, loggerFactory, []migrate.Migration{m1, m2}, candidate) err := runner.Run(t.Context()) require.NoError(t, err) @@ -272,7 +271,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { t.Run("does not update revision after failed migration", func(t *testing.T) { root := uuid.NewString() - electionSvc := election.NewService(election.NewElectionStore(client, root), logger) + electionSvc := election.NewService(election.NewElectionStore(client, root), loggerFactory) store := migrate.NewStore(client, root) i := do.New() @@ -280,7 +279,7 @@ func TestRunnerMigrationOrdering(t *testing.T) { m2 := &runnerMockMigration{id: "migration-2", err: errors.New("failed")} candidate := testCandidate(t, electionSvc, "host-1") - runner := migrate.NewRunner("host-1", store, i, logger, []migrate.Migration{m1, m2}, candidate) + runner := migrate.NewRunner("host-1", store, i, loggerFactory, []migrate.Migration{m1, m2}, candidate) err := runner.Run(t.Context()) assert.ErrorContains(t, err, "failed") diff --git a/server/internal/scheduler/provide.go b/server/internal/scheduler/provide.go index 8229b728..cb234f60 100644 --- a/server/internal/scheduler/provide.go +++ b/server/internal/scheduler/provide.go @@ -3,13 +3,13 @@ package scheduler import ( "time" - "github.com/rs/zerolog" "github.com/samber/do" clientv3 "go.etcd.io/etcd/client/v3" "github.com/pgEdge/control-plane/server/internal/config" "github.com/pgEdge/control-plane/server/internal/database" "github.com/pgEdge/control-plane/server/internal/election" + "github.com/pgEdge/control-plane/server/internal/logging" "github.com/pgEdge/control-plane/server/internal/workflows" ) @@ -63,7 +63,7 @@ func provideService(i *do.Injector) { if err != nil { return nil, err } - logger, err := do.Invoke[zerolog.Logger](i) + loggerFactory, err := do.Invoke[*logging.Factory](i) if err != nil { return nil, err } @@ -75,7 +75,7 @@ func provideService(i *do.Injector) { if err != nil { return nil, err } - return NewService(logger, store, executor, client, elector), nil + return NewService(loggerFactory, store, executor, client, elector), nil }) } diff --git a/server/internal/scheduler/service.go b/server/internal/scheduler/service.go index 2300508b..463dc0a9 100644 --- a/server/internal/scheduler/service.go +++ b/server/internal/scheduler/service.go @@ -9,9 +9,11 @@ import ( "time" "github.com/go-co-op/gocron" - "github.com/pgEdge/control-plane/server/internal/storage" "github.com/rs/zerolog" clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/pgEdge/control-plane/server/internal/logging" + "github.com/pgEdge/control-plane/server/internal/storage" ) type Service struct { @@ -29,14 +31,14 @@ type Service struct { // NewService initializes a new scheduled job service with a scheduler and job store. func NewService( - logger zerolog.Logger, + loggerFactory *logging.Factory, store *ScheduledJobStore, executor WorkflowExecutor, etcdClient *clientv3.Client, elector *Elector, ) *Service { return &Service{ - logger: logger.With().Str("component", "scheduler_service").Logger(), + logger: loggerFactory.Logger("scheduler_service"), store: store, executor: executor, etcdClient: etcdClient, diff --git a/server/internal/testutils/logger.go b/server/internal/testutils/logger.go index dfff714e..395d9374 100644 --- a/server/internal/testutils/logger.go +++ b/server/internal/testutils/logger.go @@ -3,6 +3,8 @@ package testutils import ( "testing" + "github.com/pgEdge/control-plane/server/internal/config" + "github.com/pgEdge/control-plane/server/internal/logging" "github.com/rs/zerolog" ) @@ -15,3 +17,14 @@ func Logger(t testing.TB) zerolog.Logger { return zerolog.Nop() } + +func LoggerFactory(t testing.TB) *logging.Factory { + t.Helper() + + factory, err := logging.NewFactory(config.Config{}, Logger(t)) + if err != nil { + t.Fatal(err) + } + + return factory +} diff --git a/server/internal/workflows/provide.go b/server/internal/workflows/provide.go index 8e0160c6..8d8ad655 100644 --- a/server/internal/workflows/provide.go +++ b/server/internal/workflows/provide.go @@ -25,7 +25,7 @@ func Provide(i *do.Injector) { func provideWorker(i *do.Injector) { do.Provide(i, func(i *do.Injector) (*Worker, error) { - logger, err := do.Invoke[zerolog.Logger](i) + loggerFactory, err := do.Invoke[*logging.Factory](i) if err != nil { return nil, err } @@ -41,7 +41,7 @@ func provideWorker(i *do.Injector) { if err != nil { return nil, err } - return NewWorker(logger, be, workflows, orch) + return NewWorker(loggerFactory, be, workflows, orch) }) } diff --git a/server/internal/workflows/worker.go b/server/internal/workflows/worker.go index 3743b360..1b5c9254 100644 --- a/server/internal/workflows/worker.go +++ b/server/internal/workflows/worker.go @@ -10,6 +10,8 @@ import ( "github.com/cschleiden/go-workflows/workflow" "github.com/rs/zerolog" "github.com/samber/do" + + "github.com/pgEdge/control-plane/server/internal/logging" ) var _ do.Shutdownable = (*Worker)(nil) @@ -26,7 +28,7 @@ type Worker struct { cancel context.CancelFunc } -func NewWorker(logger zerolog.Logger, be backend.Backend, workflows *Workflows, orch Orchestrator) (*Worker, error) { +func NewWorker(loggerFactory *logging.Factory, be backend.Backend, workflows *Workflows, orch Orchestrator) (*Worker, error) { queues, err := orch.WorkerQueues() if err != nil { return nil, fmt.Errorf("failed to get worker queues: %w", err) @@ -44,9 +46,7 @@ func NewWorker(logger zerolog.Logger, be backend.Backend, workflows *Workflows, } return &Worker{ - logger: logger.With(). - Str("component", "workflows_worker"). - Logger(), + logger: loggerFactory.Logger("workflows_worker"), worker: w, workflows: workflows, }, nil