| /* |
| * Copyright 2019-present Ciena Corporation |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package commands |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| flags "github.com/jessevdk/go-flags" |
| "github.com/opencord/voltctl/pkg/format" |
| "github.com/opencord/voltctl/pkg/model" |
| "github.com/opencord/voltha-lib-go/v3/pkg/config" |
| "github.com/opencord/voltha-lib-go/v3/pkg/db/kvstore" |
| "github.com/opencord/voltha-lib-go/v3/pkg/log" |
| "io/ioutil" |
| "os" |
| "strings" |
| ) |
| |
| const ( |
| defaultComponentName = "global" |
| defaultPackageName = "default" |
| ) |
| |
| // LogLevelOutput represents the output structure for the loglevel |
| type LogLevelOutput struct { |
| ComponentName string |
| Status string |
| Error string |
| } |
| |
| // SetLogLevelOpts represents the supported CLI arguments for the loglevel set command |
| type SetLogLevelOpts struct { |
| OutputOptions |
| Args struct { |
| Level string |
| Component []string |
| } `positional-args:"yes" required:"yes"` |
| } |
| |
| // ListLogLevelOpts represents the supported CLI arguments for the loglevel list command |
| type ListLogLevelsOpts struct { |
| ListOutputOptions |
| Args struct { |
| Component []string |
| } `positional-args:"yes" required:"yes"` |
| } |
| |
| // ClearLogLevelOpts represents the supported CLI arguments for the loglevel clear command |
| type ClearLogLevelsOpts struct { |
| OutputOptions |
| Args struct { |
| Component []string |
| } `positional-args:"yes" required:"yes"` |
| } |
| |
| // LogLevelOpts represents the loglevel commands |
| type LogLevelOpts struct { |
| SetLogLevel SetLogLevelOpts `command:"set"` |
| ListLogLevels ListLogLevelsOpts `command:"list"` |
| ClearLogLevels ClearLogLevelsOpts `command:"clear"` |
| } |
| |
| var logLevelOpts = LogLevelOpts{} |
| |
| const ( |
| DEFAULT_LOGLEVELS_FORMAT = "table{{ .ComponentName }}\t{{.PackageName}}\t{{.Level}}" |
| DEFAULT_LOGLEVEL_RESULT_FORMAT = "table{{ .ComponentName }}\t{{.Status}}\t{{.Error}}" |
| ) |
| |
| // RegisterLogLevelCommands is used to register set,list and clear loglevel of components |
| func RegisterLogLevelCommands(parent *flags.Parser) { |
| _, err := parent.AddCommand("loglevel", "loglevel commands", "list, set, clear log levels of components", &logLevelOpts) |
| if err != nil { |
| Error.Fatalf("Unable to register log level commands with voltctl command parser: %s", err.Error()) |
| } |
| } |
| |
| // processComponentListArgs stores the component name and package names given in command arguments to LogLevel |
| // It checks the given argument has # key or not, if # is present then split the argument for # then stores first part as component name |
| // and second part as package name |
| func processComponentListArgs(Components []string) ([]model.LogLevel, error) { |
| |
| var logLevelConfig []model.LogLevel |
| |
| if len(Components) == 0 { |
| Components = append(Components, defaultComponentName) |
| } |
| |
| for _, component := range Components { |
| logConfig := model.LogLevel{} |
| val := strings.SplitN(component, "#", 2) |
| |
| if strings.Contains(val[0], "/") { |
| return nil, errors.New("the component name '" + val[0] + "' contains an invalid character '/'") |
| } |
| |
| if len(val) > 1 { |
| if val[0] == defaultComponentName { |
| return nil, errors.New("global level doesn't support packageName") |
| } |
| logConfig.ComponentName = val[0] |
| logConfig.PackageName = strings.ReplaceAll(val[1], "/", "#") |
| } else { |
| logConfig.ComponentName = component |
| logConfig.PackageName = defaultPackageName |
| } |
| logLevelConfig = append(logLevelConfig, logConfig) |
| } |
| return logLevelConfig, nil |
| } |
| |
| // This method set loglevel for components. |
| // For example, using below command loglevel can be set for specific component with default packageName |
| // voltctl loglevel set level <componentName> |
| // For example, using below command loglevel can be set for specific component with specific packageName |
| // voltctl loglevel set level <componentName#packageName> |
| // For example, using below command loglevel can be set for more than one component for default package and other component for specific packageName |
| // voltctl loglevel set level <componentName1#packageName> <componentName2> |
| func (options *SetLogLevelOpts) Execute(args []string) error { |
| var ( |
| logLevelConfig []model.LogLevel |
| err error |
| ) |
| ProcessGlobalOptions() |
| |
| /* |
| * TODO: VOL-2738 |
| * EVIL HACK ALERT |
| * =============== |
| * It would be nice if we could squelch all but fatal log messages from |
| * the underlying libraries because as a CLI client we don't want a |
| * bunch of logs and stack traces output and instead want to deal with |
| * simple error propagation. To work around this, voltha-lib-go logging |
| * is set to fatal and we redirect etcd client logging to a temp file. |
| * |
| * Replacing os.Stderr is used here as opposed to Dup2 because we want |
| * low level panic to be displayed if they occurr. A temp file is used |
| * as opposed to /dev/null because it can't be assumed that /dev/null |
| * exists on all platforms and thus a temp file seems more portable. |
| */ |
| log.SetAllLogLevel(log.FatalLevel) |
| saveStderr := os.Stderr |
| if tmpStderr, err := ioutil.TempFile("", ""); err == nil { |
| os.Stderr = tmpStderr |
| defer func() { |
| os.Stderr = saveStderr |
| // Ignore errors on clean up because we can't do |
| // anything anyway. |
| _ = tmpStderr.Close() |
| _ = os.Remove(tmpStderr.Name()) |
| }() |
| } |
| |
| if options.Args.Level != "" { |
| if _, err := log.StringToLogLevel(options.Args.Level); err != nil { |
| return fmt.Errorf("Unknown log level '%s'. Allowed values are DEBUG, INFO, WARN, ERROR, FATAL", options.Args.Level) |
| } |
| } |
| |
| logLevelConfig, err = processComponentListArgs(options.Args.Component) |
| if err != nil { |
| return fmt.Errorf(err.Error()) |
| } |
| |
| client, err := kvstore.NewEtcdClient(GlobalConfig.KvStore, int(GlobalConfig.KvStoreConfig.Timeout.Seconds())) |
| if err != nil { |
| return fmt.Errorf("Unable to create kvstore client %s", err) |
| } |
| defer client.Close() |
| |
| // Already error checked during option processing |
| host, port, _ := splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort) |
| cm := config.NewConfigManager(client, supportedKvStoreType, host, port, int(GlobalConfig.KvStoreConfig.Timeout.Seconds())) |
| |
| var output []LogLevelOutput |
| |
| ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.KvStoreConfig.Timeout) |
| defer cancel() |
| |
| for _, lConfig := range logLevelConfig { |
| |
| logConfig := cm.InitComponentConfig(lConfig.ComponentName, config.ConfigTypeLogLevel) |
| err := logConfig.Save(ctx, lConfig.PackageName, strings.ToUpper(options.Args.Level)) |
| if err != nil { |
| output = append(output, LogLevelOutput{ComponentName: lConfig.ComponentName, Status: "Failure", Error: err.Error()}) |
| } else { |
| output = append(output, LogLevelOutput{ComponentName: lConfig.ComponentName, Status: "Success"}) |
| } |
| |
| } |
| |
| outputFormat := CharReplacer.Replace(options.Format) |
| if outputFormat == "" { |
| outputFormat = GetCommandOptionWithDefault("loglevel-set", "format", DEFAULT_LOGLEVEL_RESULT_FORMAT) |
| } |
| result := CommandResult{ |
| Format: format.Format(outputFormat), |
| OutputAs: toOutputType(options.OutputAs), |
| NameLimit: options.NameLimit, |
| Data: output, |
| } |
| |
| GenerateOutput(&result) |
| return nil |
| } |
| |
| // This method list loglevel for components. |
| // For example, using below command loglevel can be list for specific component |
| // voltctl loglevel list <componentName> |
| // For example, using below command loglevel can be list for all the components with all the packageName |
| // voltctl loglevel list |
| func (options *ListLogLevelsOpts) Execute(args []string) error { |
| |
| var ( |
| // Initialize to empty as opposed to nil so that -o json will |
| // display empty list and not null VOL-2742 |
| data []model.LogLevel = []model.LogLevel{} |
| componentList []string |
| logLevelConfig map[string]string |
| err error |
| ) |
| ProcessGlobalOptions() |
| |
| /* |
| * TODO: VOL-2738 |
| * EVIL HACK ALERT |
| * =============== |
| * It would be nice if we could squelch all but fatal log messages from |
| * the underlying libraries because as a CLI client we don't want a |
| * bunch of logs and stack traces output and instead want to deal with |
| * simple error propagation. To work around this, voltha-lib-go logging |
| * is set to fatal and we redirect etcd client logging to a temp file. |
| * |
| * Replacing os.Stderr is used here as opposed to Dup2 because we want |
| * low level panic to be displayed if they occurr. A temp file is used |
| * as opposed to /dev/null because it can't be assumed that /dev/null |
| * exists on all platforms and thus a temp file seems more portable. |
| */ |
| log.SetAllLogLevel(log.FatalLevel) |
| saveStderr := os.Stderr |
| if tmpStderr, err := ioutil.TempFile("", ""); err == nil { |
| os.Stderr = tmpStderr |
| defer func() { |
| os.Stderr = saveStderr |
| // Ignore errors on clean up because we can't do |
| // anything anyway. |
| _ = tmpStderr.Close() |
| _ = os.Remove(tmpStderr.Name()) |
| }() |
| } |
| |
| client, err := kvstore.NewEtcdClient(GlobalConfig.KvStore, int(GlobalConfig.KvStoreConfig.Timeout.Seconds())) |
| if err != nil { |
| return fmt.Errorf("Unable to create kvstore client %s", err) |
| } |
| defer client.Close() |
| |
| // Already error checked during option processing |
| host, port, _ := splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort) |
| cm := config.NewConfigManager(client, supportedKvStoreType, host, port, int(GlobalConfig.KvStoreConfig.Timeout.Seconds())) |
| |
| ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.KvStoreConfig.Timeout) |
| defer cancel() |
| |
| if len(options.Args.Component) == 0 { |
| componentList, err = cm.RetrieveComponentList(ctx, config.ConfigTypeLogLevel) |
| if err != nil { |
| return fmt.Errorf("Unable to retrieve list of voltha components : %s ", err) |
| } |
| } else { |
| componentList = options.Args.Component |
| } |
| |
| for _, componentName := range componentList { |
| logConfig := cm.InitComponentConfig(componentName, config.ConfigTypeLogLevel) |
| |
| logLevelConfig, err = logConfig.RetrieveAll(ctx) |
| if err != nil { |
| return fmt.Errorf("Unable to retrieve loglevel configuration for component %s : %s", componentName, err) |
| } |
| |
| for packageName, level := range logLevelConfig { |
| logLevel := model.LogLevel{} |
| if packageName == "" { |
| continue |
| } |
| |
| pName := strings.ReplaceAll(packageName, "#", "/") |
| logLevel.PopulateFrom(componentName, pName, level) |
| data = append(data, logLevel) |
| } |
| } |
| |
| outputFormat := CharReplacer.Replace(options.Format) |
| if outputFormat == "" { |
| outputFormat = GetCommandOptionWithDefault("loglevel-list", "format", DEFAULT_LOGLEVELS_FORMAT) |
| } |
| orderBy := options.OrderBy |
| if orderBy == "" { |
| orderBy = GetCommandOptionWithDefault("loglevel-list", "order", "") |
| } |
| |
| result := CommandResult{ |
| Format: format.Format(outputFormat), |
| Filter: options.Filter, |
| OrderBy: orderBy, |
| OutputAs: toOutputType(options.OutputAs), |
| NameLimit: options.NameLimit, |
| Data: data, |
| } |
| GenerateOutput(&result) |
| return nil |
| } |
| |
| // This method clear loglevel for components. |
| // For example, using below command loglevel can be clear for specific component with default packageName |
| // voltctl loglevel clear <componentName> |
| // For example, using below command loglevel can be clear for specific component with specific packageName |
| // voltctl loglevel clear <componentName#packageName> |
| func (options *ClearLogLevelsOpts) Execute(args []string) error { |
| |
| var ( |
| logLevelConfig []model.LogLevel |
| err error |
| ) |
| ProcessGlobalOptions() |
| |
| /* |
| * TODO: VOL-2738 |
| * EVIL HACK ALERT |
| * =============== |
| * It would be nice if we could squelch all but fatal log messages from |
| * the underlying libraries because as a CLI client we don't want a |
| * bunch of logs and stack traces output and instead want to deal with |
| * simple error propagation. To work around this, voltha-lib-go logging |
| * is set to fatal and we redirect etcd client logging to a temp file. |
| * |
| * Replacing os.Stderr is used here as opposed to Dup2 because we want |
| * low level panic to be displayed if they occurr. A temp file is used |
| * as opposed to /dev/null because it can't be assumed that /dev/null |
| * exists on all platforms and thus a temp file seems more portable. |
| */ |
| log.SetAllLogLevel(log.FatalLevel) |
| saveStderr := os.Stderr |
| if tmpStderr, err := ioutil.TempFile("", ""); err == nil { |
| os.Stderr = tmpStderr |
| defer func() { |
| os.Stderr = saveStderr |
| // Ignore errors on clean up because we can't do |
| // anything anyway. |
| _ = tmpStderr.Close() |
| _ = os.Remove(tmpStderr.Name()) |
| }() |
| } |
| |
| logLevelConfig, err = processComponentListArgs(options.Args.Component) |
| if err != nil { |
| return fmt.Errorf("%s", err) |
| } |
| |
| client, err := kvstore.NewEtcdClient(GlobalConfig.KvStore, int(GlobalConfig.KvStoreConfig.Timeout.Seconds())) |
| if err != nil { |
| return fmt.Errorf("Unable to create kvstore client %s", err) |
| } |
| defer client.Close() |
| |
| // Already error checked during option processing |
| host, port, _ := splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort) |
| cm := config.NewConfigManager(client, supportedKvStoreType, host, port, int(GlobalConfig.KvStoreConfig.Timeout.Seconds())) |
| |
| var output []LogLevelOutput |
| |
| ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.KvStoreConfig.Timeout) |
| defer cancel() |
| |
| for _, lConfig := range logLevelConfig { |
| |
| if lConfig.ComponentName == defaultComponentName { |
| return fmt.Errorf("The global default loglevel cannot be cleared.") |
| } |
| |
| logConfig := cm.InitComponentConfig(lConfig.ComponentName, config.ConfigTypeLogLevel) |
| |
| err := logConfig.Delete(ctx, lConfig.PackageName) |
| if err != nil { |
| output = append(output, LogLevelOutput{ComponentName: lConfig.ComponentName, Status: "Failure", Error: err.Error()}) |
| } else { |
| output = append(output, LogLevelOutput{ComponentName: lConfig.ComponentName, Status: "Success"}) |
| } |
| } |
| |
| outputFormat := CharReplacer.Replace(options.Format) |
| if outputFormat == "" { |
| outputFormat = GetCommandOptionWithDefault("loglevel-clear", "format", DEFAULT_LOGLEVEL_RESULT_FORMAT) |
| } |
| |
| result := CommandResult{ |
| Format: format.Format(outputFormat), |
| OutputAs: toOutputType(options.OutputAs), |
| NameLimit: options.NameLimit, |
| Data: output, |
| } |
| |
| GenerateOutput(&result) |
| return nil |
| } |