github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/master/config.go (about)

     1  // Copyright 2019 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package master
    15  
    16  import (
    17  	"bytes"
    18  	_ "embed"
    19  	"encoding/hex"
    20  	"encoding/json"
    21  	"flag"
    22  	"fmt"
    23  	"net"
    24  	"net/url"
    25  	"os"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/BurntSushi/toml"
    30  	"github.com/pingcap/tiflow/dm/config/security"
    31  	"github.com/pingcap/tiflow/dm/pkg/log"
    32  	"github.com/pingcap/tiflow/dm/pkg/terror"
    33  	"github.com/pingcap/tiflow/dm/pkg/utils"
    34  	"github.com/pingcap/tiflow/pkg/version"
    35  	"go.etcd.io/etcd/server/v3/embed"
    36  	"go.uber.org/zap"
    37  )
    38  
    39  const (
    40  	defaultRPCTimeout              = "30s"
    41  	defaultNamePrefix              = "dm-master"
    42  	defaultDataDirPrefix           = "default"
    43  	defaultPeerUrls                = "http://127.0.0.1:8291"
    44  	defaultInitialClusterState     = embed.ClusterStateFlagNew
    45  	defaultAutoCompactionMode      = "periodic"
    46  	defaultAutoCompactionRetention = "1h"
    47  	defaultMaxTxnOps               = 2048
    48  	defaultQuotaBackendBytes       = 2 * 1024 * 1024 * 1024 // 2GB
    49  	quotaBackendBytesLowerBound    = 500 * 1024 * 1024      // 500MB
    50  )
    51  
    52  // SampleConfig is sample config of dm-master.
    53  //
    54  //go:embed dm-master.toml
    55  var SampleConfig string
    56  
    57  // NewConfig creates a config for dm-master.
    58  func NewConfig() *Config {
    59  	cfg := &Config{}
    60  	cfg.flagSet = flag.NewFlagSet("dm-master", flag.ContinueOnError)
    61  	fs := cfg.flagSet
    62  
    63  	fs.BoolVar(&cfg.printVersion, "V", false, "prints version and exit")
    64  	fs.BoolVar(&cfg.printSampleConfig, "print-sample-config", false, "print sample config file of dm-worker")
    65  	fs.BoolVar(&cfg.OpenAPI, "openapi", false, "enable openapi")
    66  	fs.StringVar(&cfg.ConfigFile, "config", "", "path to config file")
    67  	fs.StringVar(&cfg.MasterAddr, "master-addr", "", "master API server and status addr")
    68  	fs.StringVar(&cfg.AdvertiseAddr, "advertise-addr", "", `advertise address for client traffic (default "${master-addr}")`)
    69  	fs.StringVar(&cfg.LogLevel, "L", "info", "log level: debug, info, warn, error, fatal")
    70  	fs.StringVar(&cfg.LogFile, "log-file", "", "log file path")
    71  	fs.StringVar(&cfg.LogFormat, "log-format", "text", `the format of the log, "text" or "json"`)
    72  	// fs.StringVar(&cfg.LogRotate, "log-rotate", "day", "log file rotate type, hour/day")
    73  
    74  	fs.StringVar(&cfg.Name, "name", "", "human-readable name for this DM-master member")
    75  	fs.StringVar(&cfg.DataDir, "data-dir", "", `path to the data directory (default "default.${name}")`)
    76  	fs.StringVar(&cfg.InitialCluster, "initial-cluster", "", fmt.Sprintf("initial cluster configuration for bootstrapping, e.g. dm-master=%s", defaultPeerUrls))
    77  	fs.StringVar(&cfg.PeerUrls, "peer-urls", defaultPeerUrls, "URLs for peer traffic")
    78  	fs.StringVar(&cfg.AdvertisePeerUrls, "advertise-peer-urls", "", `advertise URLs for peer traffic (default "${peer-urls}")`)
    79  	fs.StringVar(&cfg.Join, "join", "", `join to an existing cluster (usage: cluster's "${master-addr}" list, e.g. "127.0.0.1:8261,127.0.0.1:18261"`)
    80  	fs.UintVar(&cfg.MaxTxnOps, "max-txn-ops", defaultMaxTxnOps, `etcd's max-txn-ops, default value is 2048`)
    81  	fs.UintVar(&cfg.MaxRequestBytes, "max-request-bytes", embed.DefaultMaxRequestBytes, `etcd's max-request-bytes`)
    82  	fs.StringVar(&cfg.AutoCompactionMode, "auto-compaction-mode", defaultAutoCompactionMode, `etcd's auto-compaction-mode, either 'periodic' or 'revision'`)
    83  	fs.StringVar(&cfg.AutoCompactionRetention, "auto-compaction-retention", defaultAutoCompactionRetention, `etcd's auto-compaction-retention, accept values like '5h' or '5' (5 hours in 'periodic' mode or 5 revisions in 'revision')`)
    84  	fs.Int64Var(&cfg.QuotaBackendBytes, "quota-backend-bytes", defaultQuotaBackendBytes, `etcd's storage quota in bytes`)
    85  
    86  	fs.StringVar(&cfg.SSLCA, "ssl-ca", "", "path of file that contains list of trusted SSL CAs for connection")
    87  	fs.StringVar(&cfg.SSLCert, "ssl-cert", "", "path of file that contains X509 certificate in PEM format for connection")
    88  	fs.StringVar(&cfg.SSLKey, "ssl-key", "", "path of file that contains X509 key in PEM format for connection")
    89  	fs.Var(&cfg.CertAllowedCN, "cert-allowed-cn", "the trusted common name that allowed to visit")
    90  
    91  	fs.StringVar(&cfg.V1SourcesPath, "v1-sources-path", "", "directory path used to store source config files when upgrading from v1.0.x")
    92  	fs.StringVar(&cfg.SecretKeyPath, "secret-key-path", "", "path of file that contains secret key for encrypting and decrypting password, the secret key should be a hex AES-256 key of length 64")
    93  
    94  	return cfg
    95  }
    96  
    97  type ExperimentalFeatures struct {
    98  	OpenAPI bool `toml:"openapi,omitempty"` // OpenAPI is available in v5.4 as default.
    99  }
   100  
   101  // Config is the configuration for dm-master.
   102  type Config struct {
   103  	flagSet *flag.FlagSet
   104  
   105  	LogLevel  string `toml:"log-level" json:"log-level"`
   106  	LogFile   string `toml:"log-file" json:"log-file"`
   107  	LogFormat string `toml:"log-format" json:"log-format"`
   108  	LogRotate string `toml:"log-rotate" json:"log-rotate"`
   109  
   110  	RPCTimeoutStr string        `toml:"rpc-timeout" json:"rpc-timeout"`
   111  	RPCTimeout    time.Duration `toml:"-" json:"-"`
   112  	RPCRateLimit  float64       `toml:"rpc-rate-limit" json:"rpc-rate-limit"`
   113  	RPCRateBurst  int           `toml:"rpc-rate-burst" json:"rpc-rate-burst"`
   114  
   115  	MasterAddr    string `toml:"master-addr" json:"master-addr"`
   116  	AdvertiseAddr string `toml:"advertise-addr" json:"advertise-addr"`
   117  
   118  	ConfigFile string `toml:"config-file" json:"config-file"`
   119  
   120  	// etcd relative config items
   121  	// NOTE: we use `MasterAddr` to generate `ClientUrls` and `AdvertiseClientUrls`
   122  	// NOTE: more items will be add when adding leader election
   123  	Name                    string `toml:"name" json:"name"`
   124  	DataDir                 string `toml:"data-dir" json:"data-dir"`
   125  	PeerUrls                string `toml:"peer-urls" json:"peer-urls"`
   126  	AdvertisePeerUrls       string `toml:"advertise-peer-urls" json:"advertise-peer-urls"`
   127  	InitialCluster          string `toml:"initial-cluster" json:"initial-cluster"`
   128  	InitialClusterState     string `toml:"initial-cluster-state" json:"initial-cluster-state"`
   129  	Join                    string `toml:"join" json:"join"` // cluster's client address (endpoints), not peer address
   130  	MaxTxnOps               uint   `toml:"max-txn-ops" json:"max-txn-ops"`
   131  	MaxRequestBytes         uint   `toml:"max-request-bytes" json:"max-request-bytes"`
   132  	AutoCompactionMode      string `toml:"auto-compaction-mode" json:"auto-compaction-mode"`
   133  	AutoCompactionRetention string `toml:"auto-compaction-retention" json:"auto-compaction-retention"`
   134  	QuotaBackendBytes       int64  `toml:"quota-backend-bytes" json:"quota-backend-bytes"`
   135  	OpenAPI                 bool   `toml:"openapi" json:"openapi"`
   136  
   137  	// directory path used to store source config files when upgrading from v1.0.x.
   138  	// if this path set, DM-master leader will try to upgrade from v1.0.x to the current version.
   139  	V1SourcesPath string `toml:"v1-sources-path" json:"v1-sources-path"`
   140  
   141  	// tls config
   142  	security.Security
   143  	SecretKeyPath string `toml:"secret-key-path" json:"secret-key-path"  yaml:"secret-key-path"`
   144  	SecretKey     []byte `toml:"-" json:"-" yaml:"-"`
   145  
   146  	printVersion      bool
   147  	printSampleConfig bool
   148  
   149  	ExperimentalFeatures ExperimentalFeatures `toml:"experimental"`
   150  }
   151  
   152  func (c *Config) String() string {
   153  	cfg, err := json.Marshal(c)
   154  	if err != nil {
   155  		log.L().Error("marshal to json", zap.Reflect("master config", c), log.ShortError(err))
   156  	}
   157  	return string(cfg)
   158  }
   159  
   160  // Toml returns TOML format representation of config.
   161  func (c *Config) Toml() (string, error) {
   162  	var b bytes.Buffer
   163  
   164  	err := toml.NewEncoder(&b).Encode(c)
   165  	if err != nil {
   166  		log.L().Error("fail to marshal config to toml", log.ShortError(err))
   167  	}
   168  
   169  	return b.String(), nil
   170  }
   171  
   172  // Parse parses flag definitions from the argument list.
   173  func (c *Config) Parse(arguments []string) error {
   174  	// Parse first to get config file.
   175  	err := c.flagSet.Parse(arguments)
   176  	if err != nil {
   177  		return terror.ErrMasterConfigParseFlagSet.Delegate(err)
   178  	}
   179  
   180  	if c.printVersion {
   181  		fmt.Println(version.GetRawInfo())
   182  		return flag.ErrHelp
   183  	}
   184  
   185  	if c.printSampleConfig {
   186  		fmt.Println(SampleConfig)
   187  		return flag.ErrHelp
   188  	}
   189  
   190  	// Load config file if specified.
   191  	if c.ConfigFile != "" {
   192  		err = c.configFromFile(c.ConfigFile)
   193  		if err != nil {
   194  			return err
   195  		}
   196  	}
   197  
   198  	// Parse again to replace with command line options.
   199  	err = c.flagSet.Parse(arguments)
   200  	if err != nil {
   201  		return terror.ErrMasterConfigParseFlagSet.Delegate(err)
   202  	}
   203  
   204  	if len(c.flagSet.Args()) != 0 {
   205  		return terror.ErrMasterConfigInvalidFlag.Generate(c.flagSet.Arg(0))
   206  	}
   207  
   208  	return c.adjust()
   209  }
   210  
   211  // configFromFile loads config from file.
   212  func (c *Config) configFromFile(path string) error {
   213  	metaData, err := toml.DecodeFile(path, c)
   214  	if err != nil {
   215  		return terror.ErrMasterConfigTomlTransform.Delegate(err)
   216  	}
   217  	undecoded := metaData.Undecoded()
   218  	if len(undecoded) > 0 {
   219  		var undecodedItems []string
   220  		for _, item := range undecoded {
   221  			undecodedItems = append(undecodedItems, item.String())
   222  		}
   223  		return terror.ErrMasterConfigUnknownItem.Generate(strings.Join(undecodedItems, ","))
   224  	}
   225  	return nil
   226  }
   227  
   228  // FromContent loads config from TOML format content.
   229  func (c *Config) FromContent(content string) error {
   230  	metaData, err := toml.Decode(content, c)
   231  	if err != nil {
   232  		return terror.ErrMasterConfigTomlTransform.Delegate(err)
   233  	}
   234  	undecoded := metaData.Undecoded()
   235  	if len(undecoded) > 0 {
   236  		var undecodedItems []string
   237  		for _, item := range undecoded {
   238  			undecodedItems = append(undecodedItems, item.String())
   239  		}
   240  		return terror.ErrMasterConfigUnknownItem.Generate(strings.Join(undecodedItems, ","))
   241  	}
   242  	return c.adjust()
   243  }
   244  
   245  // adjust adjusts configs.
   246  func (c *Config) adjust() error {
   247  	c.MasterAddr = utils.UnwrapScheme(c.MasterAddr)
   248  	// MasterAddr's format may be "host:port" or ":port"
   249  	host, port, err := net.SplitHostPort(c.MasterAddr)
   250  	if err != nil {
   251  		return terror.ErrMasterHostPortNotValid.Delegate(err, c.MasterAddr)
   252  	}
   253  
   254  	if c.AdvertiseAddr == "" {
   255  		if host == "" || host == "0.0.0.0" || len(port) == 0 {
   256  			return terror.ErrMasterHostPortNotValid.Generatef("master-addr (%s) must include the 'host' part (should not be '0.0.0.0') when advertise-addr is not set", c.MasterAddr)
   257  		}
   258  		c.AdvertiseAddr = c.MasterAddr
   259  	} else {
   260  		c.AdvertiseAddr = utils.UnwrapScheme(c.AdvertiseAddr)
   261  		// AdvertiseAddr's format should be "host:port"
   262  		host, port, err = net.SplitHostPort(c.AdvertiseAddr)
   263  		if err != nil {
   264  			return terror.ErrMasterAdvertiseAddrNotValid.Delegate(err, c.AdvertiseAddr)
   265  		}
   266  		if len(host) == 0 || host == "0.0.0.0" || len(port) == 0 {
   267  			return terror.ErrMasterAdvertiseAddrNotValid.Generate(c.AdvertiseAddr)
   268  		}
   269  	}
   270  
   271  	if c.RPCTimeoutStr == "" {
   272  		c.RPCTimeoutStr = defaultRPCTimeout
   273  	}
   274  	timeout, err := time.ParseDuration(c.RPCTimeoutStr)
   275  	if err != nil {
   276  		return terror.ErrMasterConfigTimeoutParse.Delegate(err)
   277  	}
   278  	c.RPCTimeout = timeout
   279  
   280  	// for backward compatibility
   281  	if c.RPCRateLimit <= 0 {
   282  		log.L().Warn("invalid rpc-rate-limit, default value used", zap.Float64("specified rpc-rate-limit", c.RPCRateLimit), zap.Float64("default rpc-rate-limit", DefaultRate))
   283  		c.RPCRateLimit = DefaultRate
   284  	}
   285  	if c.RPCRateBurst <= 0 {
   286  		log.L().Warn("invalid rpc-rate-burst, default value use", zap.Int("specified rpc-rate-burst", c.RPCRateBurst), zap.Int("default rpc-rate-burst", DefaultBurst))
   287  		c.RPCRateBurst = DefaultBurst
   288  	}
   289  
   290  	if c.Name == "" {
   291  		var hostname string
   292  		hostname, err = os.Hostname()
   293  		if err != nil {
   294  			return terror.ErrMasterGetHostnameFail.Delegate(err)
   295  		}
   296  		c.Name = fmt.Sprintf("%s-%s", defaultNamePrefix, hostname)
   297  	}
   298  
   299  	if c.DataDir == "" {
   300  		c.DataDir = fmt.Sprintf("%s.%s", defaultDataDirPrefix, c.Name)
   301  	}
   302  
   303  	if c.PeerUrls == "" {
   304  		c.PeerUrls = defaultPeerUrls
   305  	} else {
   306  		c.PeerUrls = utils.WrapSchemes(c.PeerUrls, c.SSLCA != "")
   307  	}
   308  
   309  	if c.AdvertisePeerUrls == "" {
   310  		c.AdvertisePeerUrls = c.PeerUrls
   311  	} else {
   312  		c.AdvertisePeerUrls = utils.WrapSchemes(c.AdvertisePeerUrls, c.SSLCA != "")
   313  	}
   314  
   315  	if c.InitialCluster == "" {
   316  		items := strings.Split(c.AdvertisePeerUrls, ",")
   317  		for i, item := range items {
   318  			items[i] = fmt.Sprintf("%s=%s", c.Name, item)
   319  		}
   320  		c.InitialCluster = strings.Join(items, ",")
   321  	} else {
   322  		c.InitialCluster = utils.WrapSchemesForInitialCluster(c.InitialCluster, c.SSLCA != "")
   323  	}
   324  
   325  	if c.InitialClusterState == "" {
   326  		c.InitialClusterState = defaultInitialClusterState
   327  	}
   328  
   329  	if c.Join != "" {
   330  		c.Join = utils.WrapSchemes(c.Join, c.SSLCA != "")
   331  	}
   332  
   333  	if c.QuotaBackendBytes < quotaBackendBytesLowerBound {
   334  		log.L().Warn("quota-backend-bytes is too low, will adjust it",
   335  			zap.Int64("from", c.QuotaBackendBytes),
   336  			zap.Int64("to", quotaBackendBytesLowerBound))
   337  		c.QuotaBackendBytes = quotaBackendBytesLowerBound
   338  	}
   339  
   340  	if c.ExperimentalFeatures.OpenAPI {
   341  		c.OpenAPI = true
   342  		c.ExperimentalFeatures.OpenAPI = false
   343  		log.L().Warn("openapi is a GA feature and removed from experimental features, so this configuration may have no affect in feature release, please set openapi=true in dm-master config file")
   344  	}
   345  
   346  	return c.adjustSecretKeyPath()
   347  }
   348  
   349  func (c *Config) adjustSecretKeyPath() error {
   350  	if c.SecretKeyPath == "" {
   351  		return nil
   352  	}
   353  
   354  	content, err := os.ReadFile(c.SecretKeyPath)
   355  	if err != nil {
   356  		return terror.ErrConfigSecretKeyPath.Generate(err)
   357  	}
   358  	contentStr := strings.TrimSpace(string(content))
   359  	decodeContent, err := hex.DecodeString(contentStr)
   360  	if err != nil {
   361  		return terror.ErrConfigSecretKeyPath.Generate(err)
   362  	}
   363  	if len(decodeContent) != 32 {
   364  		return terror.ErrConfigSecretKeyPath.Generate("the secret key must be a hex AES-256 key of length 64")
   365  	}
   366  	c.SecretKey = decodeContent
   367  	return nil
   368  }
   369  
   370  // Reload load config from local file.
   371  func (c *Config) Reload() error {
   372  	if c.ConfigFile != "" {
   373  		err := c.configFromFile(c.ConfigFile)
   374  		if err != nil {
   375  			return err
   376  		}
   377  	}
   378  
   379  	return c.adjust()
   380  }
   381  
   382  // genEmbedEtcdConfig generates the configuration needed by embed etcd.
   383  // This method should be called after logger initialized and before any concurrent gRPC calls.
   384  func (c *Config) genEmbedEtcdConfig(cfg *embed.Config) (*embed.Config, error) {
   385  	cfg.Name = c.Name
   386  	cfg.Dir = c.DataDir
   387  
   388  	// reuse the previous master-addr as the client listening URL.
   389  	var err error
   390  	cfg.ListenClientUrls, err = parseURLs(c.MasterAddr)
   391  	if err != nil {
   392  		return nil, terror.ErrMasterGenEmbedEtcdConfigFail.Delegate(err, "invalid master-addr")
   393  	}
   394  	cfg.AdvertiseClientUrls, err = parseURLs(c.AdvertiseAddr)
   395  	if err != nil {
   396  		return nil, terror.ErrMasterGenEmbedEtcdConfigFail.Delegate(err, "invalid advertise-addr")
   397  	}
   398  
   399  	cfg.ListenPeerUrls, err = parseURLs(c.PeerUrls)
   400  	if err != nil {
   401  		return nil, terror.ErrMasterGenEmbedEtcdConfigFail.Delegate(err, "invalid peer-urls")
   402  	}
   403  
   404  	cfg.AdvertisePeerUrls, err = parseURLs(c.AdvertisePeerUrls)
   405  	if err != nil {
   406  		return nil, terror.ErrMasterGenEmbedEtcdConfigFail.Delegate(err, "invalid advertise-peer-urls")
   407  	}
   408  
   409  	cfg.InitialCluster = c.InitialCluster
   410  	cfg.ClusterState = c.InitialClusterState
   411  	cfg.AutoCompactionMode = c.AutoCompactionMode
   412  	cfg.AutoCompactionRetention = c.AutoCompactionRetention
   413  	cfg.QuotaBackendBytes = c.QuotaBackendBytes
   414  	cfg.MaxTxnOps = c.MaxTxnOps
   415  	cfg.MaxRequestBytes = c.MaxRequestBytes
   416  
   417  	err = cfg.Validate() // verify & trigger the builder
   418  	if err != nil {
   419  		return nil, terror.ErrMasterGenEmbedEtcdConfigFail.AnnotateDelegate(err, "fail to validate embed etcd config")
   420  	}
   421  
   422  	// security config
   423  	if len(c.SSLCA) != 0 {
   424  		cfg.ClientTLSInfo.TrustedCAFile = c.SSLCA
   425  		cfg.ClientTLSInfo.CertFile = c.SSLCert
   426  		cfg.ClientTLSInfo.KeyFile = c.SSLKey
   427  
   428  		cfg.PeerTLSInfo.TrustedCAFile = c.SSLCA
   429  		cfg.PeerTLSInfo.CertFile = c.SSLCert
   430  		cfg.PeerTLSInfo.KeyFile = c.SSLKey
   431  
   432  		// NOTE: etcd only support one allowed CN
   433  		if len(c.CertAllowedCN) > 0 {
   434  			cfg.ClientTLSInfo.AllowedCN = c.CertAllowedCN[0]
   435  			cfg.PeerTLSInfo.AllowedCN = c.CertAllowedCN[0]
   436  			cfg.PeerTLSInfo.ClientCertAuth = len(c.SSLCA) != 0
   437  			cfg.ClientTLSInfo.ClientCertAuth = len(c.SSLCA) != 0
   438  		}
   439  	}
   440  
   441  	return cfg, nil
   442  }
   443  
   444  // parseURLs parse a string into multiple urls.
   445  // if the URL in the string without protocol scheme, use `http` as the default.
   446  // if no IP exists in the address, `0.0.0.0` is used.
   447  func parseURLs(s string) ([]url.URL, error) {
   448  	if s == "" {
   449  		return nil, nil
   450  	}
   451  
   452  	items := strings.Split(s, ",")
   453  	urls := make([]url.URL, 0, len(items))
   454  	for _, item := range items {
   455  		// tolerate valid `master-addr`, but invalid URL format. mainly caused by no protocol scheme
   456  		if !(strings.HasPrefix(item, "http://") || strings.HasPrefix(item, "https://")) {
   457  			prefix := "http://"
   458  			if useTLS.Load() {
   459  				prefix = "https://"
   460  			}
   461  			item = prefix + item
   462  		}
   463  		u, err := url.Parse(item)
   464  		if err != nil {
   465  			return nil, terror.ErrMasterParseURLFail.Delegate(err, item)
   466  		}
   467  		if strings.Index(u.Host, ":") == 0 {
   468  			u.Host = "0.0.0.0" + u.Host
   469  		}
   470  		urls = append(urls, *u)
   471  	}
   472  	return urls, nil
   473  }
   474  
   475  func genEmbedEtcdConfigWithLogger(logLevel string) *embed.Config {
   476  	cfg := embed.NewConfig()
   477  	// disable grpc gateway because https://github.com/etcd-io/etcd/issues/12713
   478  	// TODO: wait above issue fixed
   479  	cfg.EnableGRPCGateway = false // enable gRPC gateway for the internal etcd.
   480  
   481  	// use zap as the logger for embed etcd
   482  	// NOTE: `genEmbedEtcdConfig` can only be called after logger initialized.
   483  	// NOTE: if using zap logger for etcd, must build it before any concurrent gRPC calls,
   484  	// otherwise, DATA RACE occur in NewZapCoreLoggerBuilder and gRPC.
   485  	logger := log.L().WithFields(zap.String("component", "embed etcd"))
   486  	// if logLevel is info, set etcd log level to WARN to reduce log
   487  	if strings.ToLower(logLevel) == "info" {
   488  		log.L().Info("Set log level of etcd to `warn`, if you want to log more message about etcd, change log-level to `debug` in master configuration file")
   489  		logger.Logger = logger.WithOptions(zap.IncreaseLevel(zap.WarnLevel))
   490  	}
   491  	cfg.ZapLoggerBuilder = embed.NewZapCoreLoggerBuilder(logger.Logger, logger.Core(), log.Props().Syncer) // use global app props.
   492  	cfg.Logger = "zap"
   493  	// TODO: we run ZapLoggerBuilder to set SetLoggerV2 before we do some etcd operations
   494  	//       otherwise we will meet data race while running `grpclog.SetLoggerV2`
   495  	//       It's vert tricky here, we should use a better way to avoid this in the future.
   496  	err := cfg.ZapLoggerBuilder(cfg)
   497  	if err != nil {
   498  		panic(err) // we must ensure we can generate embed etcd config
   499  	}
   500  
   501  	return cfg
   502  }