Repository: kubernetes-sigs/node-feature-discovery
Pull request #450 (2021-02-24 20:19:33)

nfd-worker: switch to klog
Greatly expands logging capabilities and flexibility with verbosity options, among other things.

Also, implement dynamic configuration of klog. I.e. make it possible to dynamically (at run-time) alter most of the logging configuration from the config file.


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/node-feature-discovery/pull/450#" title="Author self-approved">marquiz</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fnode-feature-discovery).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/node-feature-discovery/blob/master/OWNERS)~~ [marquiz]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
marquiz: Have to sleep over this, too

/hold
marquiz: Ach, and missing update of the sample config...
marquiz: Ready for full review (I think)
/unhold
ArangoGutierrez: Before: 
```bash
2021/02/25 13:35:08 pci-1013.present = true                                                                                                                                                   
2021/02/25 13:35:08 pci-8086.present = true                                                                                                                                                   2021/02/25 13:35:08 storage-nonrotationaldisk = true                                                                                                                                          2021/02/25 13:35:08 system-os_release.VERSION_ID = 4.7                                                                                                                                        
2021/02/25 13:35:08 system-os_release.VERSION_ID.major = 4                                                                                                                                    
2021/02/25 13:35:08 system-os_release.VERSION_ID.minor = 7                                                                                                                                    
2021/02/25 13:35:08 system-os_release.ID = rhcos                                                                                                                                              
2021/02/25 13:35:08 DEBUG: getting files in /etc/kubernetes/node-feature-discovery/custom.d                                                                                                   
2021/02/25 13:35:08 DEBUG: custom config directory "/etc/kubernetes/node-feature-discovery/custom.d" does not exist                                                                           
2021/02/25 13:35:08 INFO: Custom features: [{Name:rdma.capable Value:<nil> MatchOn:[{PciID:0xc0000718b0 UsbID:<nil> LoadedKMod:<nil> CpuID:<nil> Kconfig:<nil> Nodename:<nil>}]} {Name:rdma.av
ailable Value:<nil> MatchOn:[{PciID:<nil> UsbID:<nil> LoadedKMod:0xc000171540 CpuID:<nil> Kconfig:<nil> Nodename:<nil>}]}]                                                                    
2021/02/25 13:35:08 custom-rdma.available = true                                                                                                                                              
2021/02/25 13:35:08 Sending labeling request to nfd-master 
```
after 
```bash
I0225 13:42:58.427197       1 nfd-worker.go:520] kernel-version.revision = 0                                                                                                                  
I0225 13:42:58.427209       1 nfd-worker.go:520] kernel-selinux.enabled = true                                                                                                                
I0225 13:42:58.427216       1 nfd-worker.go:520] kernel-version.full = 4.18.0-240.10.1.el8_3.x86_64                                                                                           
I0225 13:42:58.427222       1 nfd-worker.go:520] kernel-version.major = 4                                                                                                                     
I0225 13:42:58.427228       1 nfd-worker.go:520] kernel-version.minor = 18                                                                                                                    
I0225 13:42:58.431481       1 nfd-worker.go:520] pci-1013.present = true                                                                                                                      
I0225 13:42:58.431505       1 nfd-worker.go:520] pci-8086.present = true                                                                                                                      
I0225 13:42:58.431650       1 nfd-worker.go:520] storage-nonrotationaldisk = true                                                                                                             
I0225 13:42:58.431882       1 nfd-worker.go:520] system-os_release.VERSION_ID.major = 4                                                                                                       
I0225 13:42:58.431895       1 nfd-worker.go:520] system-os_release.ID = rhcos                                                                                                                 
I0225 13:42:58.431902       1 nfd-worker.go:520] system-os_release.VERSION_ID = 4.7                                                                                                           
I0225 13:42:58.431910       1 nfd-worker.go:520] system-os_release.VERSION_ID.minor = 7                                                                                                       
I0225 13:42:58.433084       1 nfd-worker.go:520] custom-rdma.available = true                                                                                                                 
I0225 13:42:58.433179       1 nfd-worker.go:599] Sending labeling request to nfd-master 
```


Patch:
diff --git a/cmd/nfd-worker/main.go b/cmd/nfd-worker/main.go
index 3182a6a53..b3575e242 100644
--- a/cmd/nfd-worker/main.go
+++ b/cmd/nfd-worker/main.go
@@ -19,8 +19,10 @@ package main
 import (
 	"flag"
 	"fmt"
-	"log"
 	"os"
+	"strings"
+
+	"k8s.io/klog/v2"
 
 	worker "sigs.k8s.io/node-feature-discovery/pkg/nfd-worker"
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
@@ -46,17 +48,17 @@ func main() {
 
 	// Assert that the version is known
 	if version.Undefined() {
-		log.Printf("WARNING: version not set! Set -ldflags \"-X sigs.k8s.io/node-feature-discovery/pkg/version.version=`git describe --tags --dirty --always`\" during build or run.")
+		klog.Warningf("version not set! Set -ldflags \"-X sigs.k8s.io/node-feature-discovery/pkg/version.version=`git describe --tags --dirty --always`\" during build or run.")
 	}
 
 	// Get new NfdWorker instance
 	instance, err := worker.NewNfdWorker(args)
 	if err != nil {
-		log.Fatalf("Failed to initialize NfdWorker instance: %v", err)
+		klog.Fatalf("Failed to initialize NfdWorker instance: %v", err)
 	}
 
 	if err = instance.Run(); err != nil {
-		log.Fatalf("ERROR: %v", err)
+		klog.Fatal(err)
 	}
 }
 
@@ -76,13 +78,13 @@ func parseArgs(flags *flag.FlagSet, osArgs ...string) *worker.Args {
 		case "no-publish":
 			args.Overrides.NoPublish = overrides.NoPublish
 		case "label-whitelist":
-			log.Printf("WARNING: --label-whitelist is deprecated, use 'core.labelWhiteList' option in the config file, instead")
+			klog.Warningf("--label-whitelist is deprecated, use 'core.labelWhiteList' option in the config file, instead")
 			args.Overrides.LabelWhiteList = overrides.LabelWhiteList
 		case "sleep-interval":
-			log.Printf("WARNING: --sleep-interval is deprecated, use 'core.sleepInterval' option in the config file, instead")
+			klog.Warningf("--sleep-interval is deprecated, use 'core.sleepInterval' option in the config file, instead")
 			args.Overrides.SleepInterval = overrides.SleepInterval
 		case "sources":
-			log.Printf("WARNING: --sources is deprecated, use 'core.sources' option in the config file, instead")
+			klog.Warningf("--sources is deprecated, use 'core.sources' option in the config file, instead")
 			args.Overrides.Sources = overrides.Sources
 		}
 	})
@@ -111,6 +113,8 @@ func initFlags(flagset *flag.FlagSet) (*worker.Args, *worker.ConfigOverrideArgs)
 	flagset.StringVar(&args.ServerNameOverride, "server-name-override", "",
 		"Hostname expected from server certificate, useful in testing")
 
+	initKlogFlags(flagset, args)
+
 	// Flags overlapping with config file options
 	overrides := &worker.ConfigOverrideArgs{
 		LabelWhiteList: &utils.RegexpVal{},
@@ -131,3 +135,24 @@ func initFlags(flagset *flag.FlagSet) (*worker.Args, *worker.ConfigOverrideArgs)
 
 	return args, overrides
 }
+
+func initKlogFlags(flagset *flag.FlagSet, args *worker.Args) {
+	args.Klog = make(map[string]*utils.KlogFlagVal)
+
+	flags := flag.NewFlagSet("klog flags", flag.ContinueOnError)
+	//flags.SetOutput(ioutil.Discard)
+	klog.InitFlags(flags)
+	flags.VisitAll(func(f *flag.Flag) {
+		name := klogConfigOptName(f.Name)
+		args.Klog[name] = utils.NewKlogFlagVal(f)
+		flagset.Var(args.Klog[name], f.Name, f.Usage)
+	})
+}
+
+func klogConfigOptName(flagName string) string {
+	split := strings.Split(flagName, "_")
+	for i, v := range split[1:] {
+		split[i+1] = strings.Title(v)
+	}
+	return strings.Join(split, "")
+}
diff --git a/deployment/node-feature-discovery/values.yaml b/deployment/node-feature-discovery/values.yaml
index 2675f1e9b..2660d3a7f 100644
--- a/deployment/node-feature-discovery/values.yaml
+++ b/deployment/node-feature-discovery/values.yaml
@@ -77,6 +77,21 @@ worker:
     #  noPublish: false
     #  sleepInterval: 60s
     #  sources: [all]
+    #  klog:
+    #    addDirHeader: false
+    #    alsologtostderr: false
+    #    logBacktraceAt:
+    #    logtostderr: true
+    #    skipHeaders: false
+    #    stderrthreshold: 2
+    #    v: 0
+    #    vmodule:
+    ##   NOTE: the following options are not dynamically run-time configurable
+    ##         and require a nfd-worker restart to take effect after being changed
+    #    logDir:
+    #    logFile:
+    #    logFileMaxSize: 1800
+    #    skipLogHeaders: false
     #sources:
     #  cpu:
     #    cpuid:
diff --git a/docs/advanced/worker-commandline-reference.md b/docs/advanced/worker-commandline-reference.md
index d33013e30..3a4fa3fb2 100644
--- a/docs/advanced/worker-commandline-reference.md
+++ b/docs/advanced/worker-commandline-reference.md
@@ -227,3 +227,84 @@ nfd-worker -sleep-interval=1h
 **DEPRECATED**: you should use the `core.sleepInterval` option in the
 configuration file, instead.
 
+### Logging
+
+The following logging-related flags are inherited from the
+[klog](https://pkg.go.dev/k8s.io/klog/v2) package.
+
+Note: The logger setup can also be specified via the `core.klog` configuration
+file options. However, the command line flags take precedence over any
+corresponding config file options specified.
+
+#### -add_dir_header
+
+If true, adds the file directory to the header of the log messages.
+
+Default: false
+
+#### -alsologtostderr
+
+Log to standard error as well as files.
+
+Default: false
+
+#### -log_backtrace_at
+
+When logging hits line file:N, emit a stack trace.
+
+Default: *empty*
+
+#### -log_dir
+
+If non-empty, write log files in this directory.
+
+Default: *empty*
+
+#### -log_file
+
+If non-empty, use this log file.
+
+Default: *empty*
+
+#### -log_file_max_size
+
+Defines the maximum size a log file can grow to. Unit is megabytes. If the
+value is 0, the maximum file size is unlimited.
+
+Default: 1800
+
+#### -logtostderr
+
+Log to standard error instead of files
+
+Default: true
+
+#### -skip_headers
+
+If true, avoid header prefixes in the log messages.
+
+Default: false
+
+#### -skip_log_headers
+
+If true, avoid headers when opening log files.
+
+Default: false
+
+#### -stderrthreshold
+
+Logs at or above this threshold go to stderr.
+
+Default: 2
+
+#### -v
+
+Number for the log level verbosity.
+
+Default: 0
+
+#### -vmodule
+
+Comma-separated list of `pattern=N` settings for file-filtered logging.
+
+Default: *empty*
diff --git a/docs/advanced/worker-configuration-reference.md b/docs/advanced/worker-configuration-reference.md
index c26d8bcc3..dfebcd0c9 100644
--- a/docs/advanced/worker-configuration-reference.md
+++ b/docs/advanced/worker-configuration-reference.md
@@ -104,6 +104,109 @@ core:
   noPublish: true
 ```
 
+### core.klog
+
+The following options specify the logger configuration. Most of which can be
+dynamically adjusted at run-time.
+
+Note: The logger options can also be specified via command line flags which
+take precedence over any corresponding config file options.
+
+#### core.klog.addDirHeader
+
+If true, adds the file directory to the header of the log messages.
+
+Default: false
+
+Run-time configurable: yes
+
+#### core.klog.alsologtostderr
+
+Log to standard error as well as files.
+
+Default: false
+
+Run-time configurable: yes
+
+#### core.klog.logBacktraceAt
+
+When logging hits line file:N, emit a stack trace.
+
+Default: *empty*
+
+Run-time configurable: yes
+
+#### core.klog.logDir
+
+If non-empty, write log files in this directory.
+
+Default: *empty*
+
+Run-time configurable: no
+
+#### core.klog.logFile
+
+If non-empty, use this log file.
+
+Default: *empty*
+
+Run-time configurable: no
+
+#### core.klog.logFileMaxSize
+
+Defines the maximum size a log file can grow to. Unit is megabytes. If the
+value is 0, the maximum file size is unlimited.
+
+Default: 1800
+
+Run-time configurable: no
+
+#### core.klog.logtostderr
+
+Log to standard error instead of files
+
+Default: true
+
+Run-time configurable: yes
+
+#### core.klog.skipHeaders
+
+If true, avoid header prefixes in the log messages.
+
+Default: false
+
+Run-time configurable: yes
+
+#### core.klog.skipLogHeaders
+
+If true, avoid headers when opening log files.
+
+Default: false
+
+Run-time configurable: no
+
+#### core.klog.stderrthreshold
+
+Logs at or above this threshold go to stderr (default 2)
+
+Run-time configurable: yes
+
+#### core.klog.v
+
+Number for the log level verbosity.
+
+Default: 0
+
+Run-time configurable: yes
+
+#### core.klog.vmodule
+
+Comma-separated list of `pattern=N` settings for file-filtered logging.
+
+Default: *empty*
+
+Run-time configurable: yes
+
 ## sources
 
 The `sources` section contains feature source specific configuration parameters.
diff --git a/nfd-daemonset-combined.yaml.template b/nfd-daemonset-combined.yaml.template
index 7c14f7c70..1a912e50a 100644
--- a/nfd-daemonset-combined.yaml.template
+++ b/nfd-daemonset-combined.yaml.template
@@ -156,6 +156,21 @@ data:
     #  noPublish: false
     #  sleepInterval: 60s
     #  sources: [all]
+    #  klog:
+    #    addDirHeader: false
+    #    alsologtostderr: false
+    #    logBacktraceAt:
+    #    logtostderr: true
+    #    skipHeaders: false
+    #    stderrthreshold: 2
+    #    v: 0
+    #    vmodule:
+    ##   NOTE: the following options are not dynamically run-time configurable
+    ##         and require a nfd-worker restart to take effect after being changed
+    #    logDir:
+    #    logFile:
+    #    logFileMaxSize: 1800
+    #    skipLogHeaders: false
     #sources:
     #  cpu:
     #    cpuid:
diff --git a/nfd-worker-daemonset.yaml.template b/nfd-worker-daemonset.yaml.template
index d9d6efbf9..603523a26 100644
--- a/nfd-worker-daemonset.yaml.template
+++ b/nfd-worker-daemonset.yaml.template
@@ -118,6 +118,21 @@ data:
     #  noPublish: false
     #  sleepInterval: 60s
     #  sources: [all]
+    #  klog:
+    #    addDirHeader: false
+    #    alsologtostderr: false
+    #    logBacktraceAt:
+    #    logtostderr: true
+    #    skipHeaders: false
+    #    stderrthreshold: 2
+    #    v: 0
+    #    vmodule:
+    ##   NOTE: the following options are not dynamically run-time configurable
+    ##         and require a nfd-worker restart to take effect after being changed
+    #    logDir:
+    #    logFile:
+    #    logFileMaxSize: 1800
+    #    skipLogHeaders: false
     #sources:
     #  cpu:
     #    cpuid:
diff --git a/nfd-worker-job.yaml.template b/nfd-worker-job.yaml.template
index 7fd8a0b65..a9c7df96c 100644
--- a/nfd-worker-job.yaml.template
+++ b/nfd-worker-job.yaml.template
@@ -128,6 +128,21 @@ data:
     #  noPublish: false
     #  sleepInterval: 60s
     #  sources: [all]
+    #  klog:
+    #    addDirHeader: false
+    #    alsologtostderr: false
+    #    logBacktraceAt:
+    #    logtostderr: true
+    #    skipHeaders: false
+    #    stderrthreshold: 2
+    #    v: 0
+    #    vmodule:
+    ##   NOTE: the following options are not dynamically run-time configurable
+    ##         and require a nfd-worker restart to take effect after being changed
+    #    logDir:
+    #    logFile:
+    #    logFileMaxSize: 1800
+    #    skipLogHeaders: false
     #sources:
     #  cpu:
     #    cpuid:
diff --git a/nfd-worker.conf.example b/nfd-worker.conf.example
index 1f60ae521..3585b6ec9 100644
--- a/nfd-worker.conf.example
+++ b/nfd-worker.conf.example
@@ -3,6 +3,21 @@
 #  noPublish: false
 #  sleepInterval: 60s
 #  sources: [all]
+#  klog:
+#    addDirHeader: false
+#    alsologtostderr: false
+#    logBacktraceAt:
+#    logtostderr: true
+#    skipHeaders: false
+#    stderrthreshold: 2
+#    v: 0
+#    vmodule:
+##   NOTE: the following options are not dynamically run-time configurable
+##         and require a nfd-worker restart to take effect after being changed
+#    logDir:
+#    logFile:
+#    logFileMaxSize: 1800
+#    skipLogHeaders: false
 #sources:
 #  cpu:
 #    cpuid:
diff --git a/pkg/nfd-worker/nfd-worker.go b/pkg/nfd-worker/nfd-worker.go
index 36beb6f8d..1ad42adde 100644
--- a/pkg/nfd-worker/nfd-worker.go
+++ b/pkg/nfd-worker/nfd-worker.go
@@ -22,7 +22,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
-	"log"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -34,6 +33,7 @@ import (
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"k8s.io/apimachinery/pkg/util/validation"
+	"k8s.io/klog/v2"
 	"sigs.k8s.io/yaml"
 
 	pb "sigs.k8s.io/node-feature-discovery/pkg/labeler"
@@ -56,9 +56,7 @@ import (
 )
 
 var (
-	stdoutLogger = log.New(os.Stdout, "", log.LstdFlags)
-	stderrLogger = log.New(os.Stderr, "", log.LstdFlags)
-	nodeName     = os.Getenv("NODE_NAME")
+	nodeName = os.Getenv("NODE_NAME")
 )
 
 // Global config
@@ -68,6 +66,7 @@ type NFDConfig struct {
 }
 
 type coreConfig struct {
+	Klog           map[string]string
 	LabelWhiteList utils.RegexpVal
 	NoPublish      bool
 	Sources        []string
@@ -90,6 +89,7 @@ type Args struct {
 	Server             string
 	ServerNameOverride string
 
+	Klog      map[string]*utils.KlogFlagVal
 	Overrides ConfigOverrideArgs
 }
 
@@ -186,9 +186,9 @@ func addConfigWatch(path string) (*fsnotify.Watcher, map[string]struct{}, error)
 	for p := path; ; p = filepath.Dir(p) {
 
 		if err := w.Add(p); err != nil {
-			stdoutLogger.Printf("failed to add fsnotify watch for %q: %v", p, err)
+			klog.V(1).Infof("failed to add fsnotify watch for %q: %v", p, err)
 		} else {
-			stdoutLogger.Printf("added fsnotify watch %q", p)
+			klog.V(1).Infof("added fsnotify watch %q", p)
 			added = true
 		}
 
@@ -211,6 +211,7 @@ func newDefaultConfig() *NFDConfig {
 			LabelWhiteList: utils.RegexpVal{Regexp: *regexp.MustCompile("")},
 			SleepInterval:  duration{60 * time.Second},
 			Sources:        []string{"all"},
+			Klog:           make(map[string]string),
 		},
 	}
 }
@@ -218,8 +219,8 @@ func newDefaultConfig() *NFDConfig {
 // Run NfdWorker client. Returns if a fatal error is encountered, or, after
 // one request if OneShot is set to 'true' in the worker args.
 func (w *nfdWorker) Run() error {
-	stdoutLogger.Printf("Node Feature Discovery Worker %s", version.Get())
-	stdoutLogger.Printf("NodeName: '%s'", nodeName)
+	klog.Infof("Node Feature Discovery Worker %s", version.Get())
+	klog.Infof("NodeName: '%s'", nodeName)
 
 	// Create watcher for config file and read initial configuration
 	configWatch, paths, err := addConfigWatch(w.configFilePath)
@@ -266,11 +267,11 @@ func (w *nfdWorker) Run() error {
 
 			// If any of our paths (directories or the file itself) change
 			if _, ok := paths[name]; ok {
-				stdoutLogger.Printf("fsnotify event in %q detected, reconfiguring fsnotify and reloading configuration", name)
+				klog.Infof("fsnotify event in %q detected, reconfiguring fsnotify and reloading configuration", name)
 
 				// Blindly remove existing watch and add a new one
 				if err := configWatch.Close(); err != nil {
-					stderrLogger.Printf("WARNING: failed to close fsnotify watcher: %v", err)
+					klog.Warningf("failed to close fsnotify watcher: %v", err)
 				}
 				configWatch, paths, err = addConfigWatch(w.configFilePath)
 				if err != nil {
@@ -284,7 +285,7 @@ func (w *nfdWorker) Run() error {
 			}
 
 		case e := <-configWatch.Errors:
-			stderrLogger.Printf("ERROR: config file watcher error: %v", e)
+			klog.Errorf("config file watcher error: %v", e)
 
 		case <-configTrigger:
 			if err := w.configure(w.configFilePath, w.args.Options); err != nil {
@@ -303,7 +304,7 @@ func (w *nfdWorker) Run() error {
 			labelTrigger = time.After(0)
 
 		case <-w.stop:
-			stdoutLogger.Printf("shutting down nfd-worker")
+			klog.Infof("shutting down nfd-worker")
 			configWatch.Close()
 			return nil
 		}
@@ -380,14 +381,32 @@ func (w *nfdWorker) disconnect() {
 
 func (c *coreConfig) sanitize() {
 	if c.SleepInterval.Duration > 0 && c.SleepInterval.Duration < time.Second {
-		stderrLogger.Printf("WARNING: too short sleep-intervall specified (%s), forcing to 1s",
+		klog.Warningf("too short sleep-intervall specified (%s), forcing to 1s",
 			c.SleepInterval.Duration.String())
 		c.SleepInterval = duration{time.Second}
 	}
 }
 
-func (w *nfdWorker) configureCore(c coreConfig) {
-	// Determine enabled feature sourcds
+func (w *nfdWorker) configureCore(c coreConfig) error {
+	// Handle klog
+	for k, a := range w.args.Klog {
+		if !a.IsSetFromCmdline() {
+			v, ok := c.Klog[k]
+			if !ok {
+				v = a.DefValue()
+			}
+			if err := a.SetFromConfig(v); err != nil {
+				return err
+			}
+		}
+	}
+	for k := range c.Klog {
+		if _, ok := w.args.Klog[k]; !ok {
+			klog.Warningf("unknown logger option in config: %q", k)
+		}
+	}
+
+	// Determine enabled feature sources
 	sourceList := map[string]struct{}{}
 	all := false
 	for _, s := range c.Sources {
@@ -416,8 +435,9 @@ func (w *nfdWorker) configureCore(c coreConfig) {
 		for n := range sourceList {
 			names = append(names, n)
 		}
-		stderrLogger.Printf("WARNING: skipping unknown source(s) %q specified in core.sources (or --sources)", strings.Join(names, ", "))
+		klog.Warningf("skipping unknown source(s) %q specified in core.sources (or --sources)", strings.Join(names, ", "))
 	}
+	return nil
 }
 
 // Parse configuration options
@@ -435,7 +455,7 @@ func (w *nfdWorker) configure(filepath string, overrides string) error {
 		data, err := ioutil.ReadFile(filepath)
 		if err != nil {
 			if os.IsNotExist(err) {
-				stderrLogger.Printf("config file %q not found, using defaults", filepath)
+				klog.Infof("config file %q not found, using defaults", filepath)
 			} else {
 				return fmt.Errorf("error reading config file: %s", err)
 			}
@@ -444,7 +464,7 @@ func (w *nfdWorker) configure(filepath string, overrides string) error {
 			if err != nil {
 				return fmt.Errorf("Failed to parse config file: %s", err)
 			}
-			stdoutLogger.Printf("Configuration successfully loaded from %q", filepath)
+			klog.Infof("Configuration successfully loaded from %q", filepath)
 		}
 	}
 
@@ -470,7 +490,9 @@ func (w *nfdWorker) configure(filepath string, overrides string) error {
 
 	w.config = c
 
-	w.configureCore(c.Core)
+	if err := w.configureCore(c.Core); err != nil {
+		return err
+	}
 
 	// (Re-)configure all "real" sources, test sources are not configurable
 	for _, s := range allSources {
@@ -489,14 +511,13 @@ func createFeatureLabels(sources []source.FeatureSource, labelWhiteList regexp.R
 	for _, source := range sources {
 		labelsFromSource, err := getFeatureLabels(source, labelWhiteList)
 		if err != nil {
-			stderrLogger.Printf("discovery failed for source [%s]: %s", source.Name(), err.Error())
-			stderrLogger.Printf("continuing ...")
+			klog.Errorf("discovery failed for source %q: %v", source.Name(), err)
 			continue
 		}
 
 		for name, value := range labelsFromSource {
 			// Log discovered feature.
-			stdoutLogger.Printf("%s = %s", name, value)
+			klog.Infof("%s = %s", name, value)
 			labels[name] = value
 		}
 	}
@@ -508,7 +529,7 @@ func createFeatureLabels(sources []source.FeatureSource, labelWhiteList regexp.R
 func getFeatureLabels(source source.FeatureSource, labelWhiteList regexp.Regexp) (labels Labels, err error) {
 	defer func() {
 		if r := recover(); r != nil {
-			stderrLogger.Printf("panic occurred during discovery of source [%s]: %v", source.Name(), r)
+			klog.Errorf("panic occurred during discovery of source [%s]: %v", source.Name(), r)
 			err = fmt.Errorf("%v", r)
 		}
 	}()
@@ -546,7 +567,7 @@ func getFeatureLabels(source source.FeatureSource, labelWhiteList regexp.Regexp)
 		// Validate label name.
 		errs := validation.IsQualifiedName(nameForValidation)
 		if len(errs) > 0 {
-			stderrLogger.Printf("Ignoring invalid feature name '%s': %s", label, errs)
+			klog.Warningf("Ignoring invalid feature name '%s': %s", label, errs)
 			continue
 		}
 
@@ -554,13 +575,13 @@ func getFeatureLabels(source source.FeatureSource, labelWhiteList regexp.Regexp)
 		// Validate label value
 		errs = validation.IsValidLabelValue(value)
 		if len(errs) > 0 {
-			stderrLogger.Printf("Ignoring invalid feature value %s=%s: %s", label, value, errs)
+			klog.Warningf("Ignoring invalid feature value %s=%s: %s", label, value, errs)
 			continue
 		}
 
 		// Skip if label doesn't match labelWhiteList
 		if !labelWhiteList.MatchString(nameForWhiteListing) {
-			stderrLogger.Printf("%q does not match the whitelist (%s) and will not be published.", nameForWhiteListing, labelWhiteList.String())
+			klog.Infof("%q does not match the whitelist (%s) and will not be published.", nameForWhiteListing, labelWhiteList.String())
 			continue
 		}
 
@@ -575,14 +596,14 @@ func advertiseFeatureLabels(client pb.LabelerClient, labels Labels) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()
 
-	stdoutLogger.Printf("Sending labeling request to nfd-master")
+	klog.Infof("Sending labeling request to nfd-master")
 
 	labelReq := pb.SetLabelsRequest{Labels: labels,
 		NfdVersion: version.Get(),
 		NodeName:   nodeName}
 	_, err := client.SetLabels(ctx, &labelReq)
 	if err != nil {
-		stderrLogger.Printf("failed to set node labels: %v", err)
+		klog.Errorf("failed to set node labels: %v", err)
 		return err
 	}
 
diff --git a/pkg/utils/flags.go b/pkg/utils/flags.go
index b91c70c7f..c88a2e59a 100644
--- a/pkg/utils/flags.go
+++ b/pkg/utils/flags.go
@@ -18,6 +18,7 @@ package utils
 
 import (
 	"encoding/json"
+	"flag"
 	"fmt"
 	"regexp"
 	"sort"
@@ -98,3 +99,64 @@ func (a *StringSliceVal) String() string {
 	}
 	return strings.Join(*a, ",")
 }
+
+// KlogFlagVal is a wrapper to allow dynamic control of klog from the config file
+type KlogFlagVal struct {
+	flag             *flag.Flag
+	isSetFromCmdLine bool
+}
+
+// Set implements flag.Value interface
+func (k *KlogFlagVal) Set(value string) error {
+	k.isSetFromCmdLine = true
+	return k.flag.Value.Set(value)
+}
+
+// String implements flag.Value interface
+func (k *KlogFlagVal) String() string {
+	if k.flag == nil {
+		return ""
+	}
+	// Need to handle "log_backtrace_at" in a special way
+	s := k.flag.Value.String()
+	if k.flag.Name == "log_backtrace_at" && s == ":0" {
+		s = ""
+	}
+	return s
+}
+
+// DefValue returns the default value of KlogFlagVal as string
+func (k *KlogFlagVal) DefValue() string {
+	// Need to handle "log_backtrace_at" in a special way
+	d := k.flag.DefValue
+	if k.flag.Name == "log_backtrace_at" && d == ":0" {
+		d = ""
+	}
+	return d
+}
+
+// SetFromConfig sets the value without marking it as set from the cmdline
+func (k *KlogFlagVal) SetFromConfig(value string) error {
+	return k.flag.Value.Set(value)
+}
+
+// IsSetFromCmdline returns true if the value has been set via Set()
+func (k *KlogFlagVal) IsSetFromCmdline() bool { return k.isSetFromCmdLine }
+
+// IsBoolFlag implements flag.boolFlag.IsBoolFlag() for wrapped klog flags.
+func (k *KlogFlagVal) IsBoolFlag() bool {
+	if ba, ok := k.flag.Value.(boolFlag); ok {
+		return ba.IsBoolFlag()
+	}
+	return false
+}
+
+// NewKlogFlagVal wraps a klog flag into KlogFlagVal type
+func NewKlogFlagVal(f *flag.Flag) *KlogFlagVal {
+	return &KlogFlagVal{flag: f}
+}
+
+// boolFlag replicates boolFlag interface internal to the flag package
+type boolFlag interface {
+	IsBoolFlag() bool
+}
diff --git a/source/cpu/cpu.go b/source/cpu/cpu.go
index 285b780ef..1bc97ba62 100644
--- a/source/cpu/cpu.go
+++ b/source/cpu/cpu.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 The Kubernetes Authors.
+Copyright 2018-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,7 +18,8 @@ package cpu
 
 import (
 	"io/ioutil"
-	"log"
+
+	"k8s.io/klog/v2"
 
 	"sigs.k8s.io/node-feature-discovery/source"
 	"sigs.k8s.io/node-feature-discovery/source/internal/cpuidutils"
@@ -96,7 +97,7 @@ func (s *Source) SetConfig(conf source.Config) {
 		s.config = v
 		s.initCpuidFilter()
 	default:
-		log.Printf("PANIC: invalid config type: %T", conf)
+		klog.Fatalf("invalid config type: %T", conf)
 	}
 }
 
@@ -106,7 +107,7 @@ func (s *Source) Discover() (source.Features, error) {
 	// Check if hyper-threading seems to be enabled
 	found, err := haveThreadSiblings()
 	if err != nil {
-		log.Printf("ERROR: failed to detect hyper-threading: %v", err)
+		klog.Errorf("failed to detect hyper-threading: %v", err)
 	} else if found {
 		features["hardware_multithreading"] = true
 	}
@@ -114,7 +115,7 @@ func (s *Source) Discover() (source.Features, error) {
 	// Check SST-BF
 	found, err = discoverSSTBF()
 	if err != nil {
-		log.Printf("ERROR: failed to detect SST-BF: %v", err)
+		klog.Errorf("failed to detect SST-BF: %v", err)
 	} else if found {
 		features["power.sst_bf.enabled"] = true
 	}
@@ -130,7 +131,7 @@ func (s *Source) Discover() (source.Features, error) {
 	// Detect pstate features
 	pstate, err := detectPstate()
 	if err != nil {
-		log.Printf("ERROR: %v", err)
+		klog.Error(err)
 	} else {
 		for k, v := range pstate {
 			features["pstate."+k] = v
diff --git a/source/custom/custom.go b/source/custom/custom.go
index a49ad510e..1d0f74bac 100644
--- a/source/custom/custom.go
+++ b/source/custom/custom.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2020 The Kubernetes Authors.
+Copyright 2020-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -17,9 +17,10 @@ limitations under the License.
 package custom
 
 import (
-	"log"
 	"reflect"
 
+	"k8s.io/klog/v2"
+
 	"sigs.k8s.io/node-feature-discovery/source"
 	"sigs.k8s.io/node-feature-discovery/source/custom/rules"
 )
@@ -67,7 +68,7 @@ func (s *Source) SetConfig(conf source.Config) {
 	case *config:
 		s.config = v
 	default:
-		log.Printf("PANIC: invalid config type: %T", conf)
+		klog.Fatalf("invalid config type: %T", conf)
 	}
 }
 
@@ -76,12 +77,12 @@ func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
 	allFeatureConfig := append(getStaticFeatureConfig(), *s.config...)
 	allFeatureConfig = append(allFeatureConfig, getDirectoryFeatureConfig()...)
-	log.Printf("INFO: Custom features: %+v", allFeatureConfig)
+	klog.V(1).Infof("Custom features: %+v", allFeatureConfig)
 	// Iterate over features
 	for _, customFeature := range allFeatureConfig {
 		featureExist, err := s.discoverFeature(customFeature)
 		if err != nil {
-			log.Printf("ERROR: failed to discover feature: %q: %s", customFeature.Name, err.Error())
+			klog.Errorf("failed to discover feature: %q: %s", customFeature.Name, err.Error())
 			continue
 		}
 		if featureExist {
diff --git a/source/custom/directory_features.go b/source/custom/directory_features.go
index 333547a55..f2aa2f01e 100644
--- a/source/custom/directory_features.go
+++ b/source/custom/directory_features.go
@@ -18,11 +18,11 @@ package custom
 
 import (
 	"io/ioutil"
-	"log"
 	"os"
 	"path/filepath"
 	"strings"
 
+	"k8s.io/klog/v2"
 	"sigs.k8s.io/yaml"
 )
 
@@ -32,20 +32,20 @@ const Directory = "/etc/kubernetes/node-feature-discovery/custom.d"
 // host directory and its 1st level subdirectories, which can be populated e.g. by ConfigMaps
 func getDirectoryFeatureConfig() []FeatureSpec {
 	features := readDir(Directory, true)
-	//log.Printf("DEBUG: all configmap based custom feature specs: %+v", features)
+	klog.V(1).Infof("all configmap based custom feature specs: %+v", features)
 	return features
 }
 
 func readDir(dirName string, recursive bool) []FeatureSpec {
 	features := make([]FeatureSpec, 0)
 
-	log.Printf("DEBUG: getting files in %s", dirName)
+	klog.V(1).Infof("getting files in %s", dirName)
 	files, err := ioutil.ReadDir(dirName)
 	if err != nil {
 		if os.IsNotExist(err) {
-			log.Printf("DEBUG: custom config directory %q does not exist", dirName)
+			klog.V(1).Infof("custom config directory %q does not exist", dirName)
 		} else {
-			log.Printf("ERROR: unable to access custom config directory %q, %v", dirName, err)
+			klog.Errorf("unable to access custom config directory %q, %v", dirName, err)
 		}
 		return features
 	}
@@ -55,30 +55,30 @@ func readDir(dirName string, recursive bool) []FeatureSpec {
 
 		if file.IsDir() {
 			if recursive {
-				//log.Printf("DEBUG: going into dir %q", fileName)
+				klog.V(1).Infof("processing dir %q", fileName)
 				features = append(features, readDir(fileName, false)...)
-				//} else {
-				//	log.Printf("DEBUG: skipping dir %q", fileName)
+			} else {
+				klog.V(2).Infof("skipping dir %q", fileName)
 			}
 			continue
 		}
 		if strings.HasPrefix(file.Name(), ".") {
-			//log.Printf("DEBUG: skipping hidden file %q", fileName)
+			klog.V(2).Infof("skipping hidden file %q", fileName)
 			continue
 		}
-		//log.Printf("DEBUG: processing file %q", fileName)
+		klog.V(2).Infof("processing file %q", fileName)
 
 		bytes, err := ioutil.ReadFile(fileName)
 		if err != nil {
-			log.Printf("ERROR: could not read custom config file %q, %v", fileName, err)
+			klog.Errorf("could not read custom config file %q, %v", fileName, err)
 			continue
 		}
-		//log.Printf("DEBUG: custom config rules raw: %s", string(bytes))
+		klog.V(2).Infof("custom config rules raw: %s", string(bytes))
 
 		config := &[]FeatureSpec{}
 		err = yaml.UnmarshalStrict(bytes, config)
 		if err != nil {
-			log.Printf("ERROR: could not parse custom config file %q, %v", fileName, err)
+			klog.Errorf("could not parse custom config file %q, %v", fileName, err)
 			continue
 		}
 
diff --git a/source/custom/rules/nodename_rule.go b/source/custom/rules/nodename_rule.go
index 744f6dd6c..7c32d9b36 100644
--- a/source/custom/rules/nodename_rule.go
+++ b/source/custom/rules/nodename_rule.go
@@ -16,9 +16,10 @@ limitations under the License.
 package rules
 
 import (
-	"log"
 	"os"
 	"regexp"
+
+	"k8s.io/klog/v2"
 )
 
 var (
@@ -33,17 +34,17 @@ var _ Rule = NodenameRule{}
 
 func (n NodenameRule) Match() (bool, error) {
 	for _, nodenamePattern := range n {
-		log.Printf("DEBUG: matchNodename %s", nodenamePattern)
+		klog.V(1).Infof("matchNodename %s", nodenamePattern)
 		match, err := regexp.MatchString(nodenamePattern, nodeName)
 		if err != nil {
-			log.Printf("ERROR: nodename rule: invalid nodename regexp %q: %v", nodenamePattern, err)
+			klog.Errorf("nodename rule: invalid nodename regexp %q: %v", nodenamePattern, err)
 			continue
 		}
 		if !match {
-			//log.Printf("DEBUG: nodename rule: No match for pattern %q with node %q", nodenamePattern, nodeName)
+			klog.V(2).Infof("nodename rule: No match for pattern %q with node %q", nodenamePattern, nodeName)
 			continue
 		}
-		//log.Printf("DEBUG: nodename rule: Match for pattern %q with node %q", nodenamePattern, nodeName)
+		klog.V(2).Infof("nodename rule: Match for pattern %q with node %q", nodenamePattern, nodeName)
 		return true, nil
 	}
 	return false, nil
diff --git a/source/internal/kernelutils/kernel_kconfig.go b/source/internal/kernelutils/kernel_kconfig.go
index 9d8187d28..6b56f6f2a 100644
--- a/source/internal/kernelutils/kernel_kconfig.go
+++ b/source/internal/kernelutils/kernel_kconfig.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2018-2020 The Kubernetes Authors.
+Copyright 2018-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -21,12 +21,13 @@ import (
 	"compress/gzip"
 	"fmt"
 	"io/ioutil"
-	"log"
 	"os"
 	"path/filepath"
 	"regexp"
 	"strings"
 
+	"k8s.io/klog/v2"
+
 	"k8s.io/apimachinery/pkg/util/validation"
 	"sigs.k8s.io/node-feature-discovery/source"
 )
@@ -109,7 +110,7 @@ func ParseKconfig(configPath string) (map[string]string, error) {
 			} else {
 				value := strings.Trim(m[2], `"`)
 				if len(value) > validation.LabelValueMaxLength {
-					log.Printf("WARNING: ignoring kconfig option '%s': value exceeds max length of %d characters", m[1], validation.LabelValueMaxLength)
+					klog.Warningf("ignoring kconfig option '%s': value exceeds max length of %d characters", m[1], validation.LabelValueMaxLength)
 					continue
 				}
 				kconfig[m[1]] = value
diff --git a/source/internal/pci_utils.go b/source/internal/pci_utils.go
index 067b2a8ec..3440ad4bf 100644
--- a/source/internal/pci_utils.go
+++ b/source/internal/pci_utils.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2020 The Kubernetes Authors.
+Copyright 2020-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,10 +19,11 @@ package busutils
 import (
 	"fmt"
 	"io/ioutil"
-	"log"
 	"path"
 	"strings"
 
+	"k8s.io/klog/v2"
+
 	"sigs.k8s.io/node-feature-discovery/source"
 )
 
@@ -88,7 +89,7 @@ func DetectPci(deviceAttrSpec map[string]bool) (map[string][]PciDeviceInfo, erro
 	for _, device := range devices {
 		info, err := readPciDevInfo(path.Join(sysfsBasePath, device.Name()), deviceAttrSpec)
 		if err != nil {
-			log.Print(err)
+			klog.Error(err)
 			continue
 		}
 		class := info["class"]
diff --git a/source/internal/usb_utils.go b/source/internal/usb_utils.go
index 9bafdf93b..e2b4e7971 100644
--- a/source/internal/usb_utils.go
+++ b/source/internal/usb_utils.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2020 The Kubernetes Authors.
+Copyright 2020-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,10 +19,11 @@ package busutils
 import (
 	"fmt"
 	"io/ioutil"
-	"log"
 	"path"
 	"path/filepath"
 	"strings"
+
+	"k8s.io/klog/v2"
 )
 
 type UsbDeviceInfo map[string]string
@@ -125,7 +126,7 @@ func DetectUsb(deviceAttrSpec map[string]bool) (map[string][]UsbDeviceInfo, erro
 	for _, device := range devices {
 		devMap, err := readUsbDevInfo(filepath.Dir(device), deviceAttrSpec)
 		if err != nil {
-			log.Print(err)
+			klog.Error(err)
 			continue
 		}
 
diff --git a/source/kernel/kernel.go b/source/kernel/kernel.go
index c6d4fce02..394d84a4a 100644
--- a/source/kernel/kernel.go
+++ b/source/kernel/kernel.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 The Kubernetes Authors.
+Copyright 2018-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -17,10 +17,11 @@ limitations under the License.
 package kernel
 
 import (
-	"log"
 	"regexp"
 	"strings"
 
+	"k8s.io/klog/v2"
+
 	"sigs.k8s.io/node-feature-discovery/source"
 	"sigs.k8s.io/node-feature-discovery/source/internal/kernelutils"
 )
@@ -63,7 +64,7 @@ func (s *Source) SetConfig(conf source.Config) {
 	case *Config:
 		s.config = v
 	default:
-		log.Printf("PANIC: invalid config type: %T", conf)
+		klog.Fatalf("invalid config type: %T", conf)
 	}
 }
 
@@ -73,7 +74,7 @@ func (s *Source) Discover() (source.Features, error) {
 	// Read kernel version
 	version, err := parseVersion()
 	if err != nil {
-		log.Printf("ERROR: Failed to get kernel version: %s", err)
+		klog.Errorf("Failed to get kernel version: %s", err)
 	} else {
 		for key := range version {
 			features["version."+key] = version[key]
@@ -83,7 +84,7 @@ func (s *Source) Discover() (source.Features, error) {
 	// Read kconfig
 	kconfig, err := kernelutils.ParseKconfig(s.config.KconfigFile)
 	if err != nil {
-		log.Printf("ERROR: Failed to read kconfig: %s", err)
+		klog.Errorf("Failed to read kconfig: %s", err)
 	}
 
 	// Check flags
@@ -95,7 +96,7 @@ func (s *Source) Discover() (source.Features, error) {
 
 	selinux, err := SelinuxEnabled()
 	if err != nil {
-		log.Print(err)
+		klog.Warning(err)
 	} else if selinux {
 		features["selinux.enabled"] = true
 	}
diff --git a/source/local/local.go b/source/local/local.go
index ade714a77..7241c9ad8 100644
--- a/source/local/local.go
+++ b/source/local/local.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 The Kubernetes Authors.
+Copyright 2018-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,12 +20,13 @@ import (
 	"bytes"
 	"fmt"
 	"io/ioutil"
-	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"strings"
 
+	"k8s.io/klog/v2"
+
 	"sigs.k8s.io/node-feature-discovery/source"
 )
 
@@ -52,18 +53,18 @@ func (s *Source) SetConfig(source.Config) {}
 func (s Source) Discover() (source.Features, error) {
 	featuresFromHooks, err := getFeaturesFromHooks()
 	if err != nil {
-		log.Printf("%v", err)
+		klog.Error(err)
 	}
 
 	featuresFromFiles, err := getFeaturesFromFiles()
 	if err != nil {
-		log.Printf("%v", err)
+		klog.Error(err)
 	}
 
 	// Merge features from hooks and files
 	for k, v := range featuresFromHooks {
 		if old, ok := featuresFromFiles[k]; ok {
-			log.Printf("WARNING: overriding label '%s': value changed from '%s' to '%s'",
+			klog.Warningf("overriding label '%s': value changed from '%s' to '%s'",
 				k, old, v)
 		}
 		featuresFromFiles[k] = v
@@ -110,7 +111,7 @@ func getFeaturesFromHooks() (source.Features, error) {
 	files, err := ioutil.ReadDir(hookDir)
 	if err != nil {
 		if os.IsNotExist(err) {
-			log.Printf("ERROR: hook directory %v does not exist", hookDir)
+			klog.Infof("hook directory %v does not exist", hookDir)
 			return features, nil
 		}
 		return features, fmt.Errorf("Unable to access %v: %v", hookDir, err)
@@ -120,14 +121,14 @@ func getFeaturesFromHooks() (source.Features, error) {
 		fileName := file.Name()
 		lines, err := runHook(fileName)
 		if err != nil {
-			log.Printf("ERROR: source local failed running hook '%v': %v", fileName, err)
+			klog.Errorf("source local failed running hook '%v': %v", fileName, err)
 			continue
 		}
 
 		// Append features
 		for k, v := range parseFeatures(lines, fileName) {
 			if old, ok := features[k]; ok {
-				log.Printf("WARNING: overriding label '%s' from another hook (%s): value changed from '%s' to '%s'",
+				klog.Warningf("overriding label '%s' from another hook (%s): value changed from '%s' to '%s'",
 					k, fileName, old, v)
 			}
 			features[k] = v
@@ -144,7 +145,7 @@ func runHook(file string) ([][]byte, error) {
 	path := filepath.Join(hookDir, file)
 	filestat, err := os.Stat(path)
 	if err != nil {
-		log.Printf("ERROR: skipping %v, failed to get stat: %v", path, err)
+		klog.Errorf("skipping %v, failed to get stat: %v", path, err)
 		return lines, err
 	}
 
@@ -165,7 +166,7 @@ func runHook(file string) ([][]byte, error) {
 				// Don't print the last empty string
 				break
 			}
-			log.Printf("%v: %s", file, line)
+			klog.Errorf("%v: %s", file, line)
 		}
 
 		// Do not return any lines if an error occurred
@@ -185,7 +186,7 @@ func getFeaturesFromFiles() (source.Features, error) {
 	files, err := ioutil.ReadDir(featureFilesDir)
 	if err != nil {
 		if os.IsNotExist(err) {
-			log.Printf("ERROR: features directory %v does not exist", featureFilesDir)
+			klog.Infof("features directory %v does not exist", featureFilesDir)
 			return features, nil
 		}
 		return features, fmt.Errorf("Unable to access %v: %v", featureFilesDir, err)
@@ -195,14 +196,14 @@ func getFeaturesFromFiles() (source.Features, error) {
 		fileName := file.Name()
 		lines, err := getFileContent(fileName)
 		if err != nil {
-			log.Printf("ERROR: source local failed reading file '%v': %v", fileName, err)
+			klog.Errorf("source local failed reading file '%v': %v", fileName, err)
 			continue
 		}
 
 		// Append features
 		for k, v := range parseFeatures(lines, fileName) {
 			if old, ok := features[k]; ok {
-				log.Printf("WARNING: overriding label '%s' from another features.d file (%s): value changed from '%s' to '%s'",
+				klog.Warningf("overriding label '%s' from another features.d file (%s): value changed from '%s' to '%s'",
 					k, fileName, old, v)
 			}
 			features[k] = v
@@ -219,7 +220,7 @@ func getFileContent(fileName string) ([][]byte, error) {
 	path := filepath.Join(featureFilesDir, fileName)
 	filestat, err := os.Stat(path)
 	if err != nil {
-		log.Printf("ERROR: skipping %v, failed to get stat: %v", path, err)
+		klog.Errorf("skipping %v, failed to get stat: %v", path, err)
 		return lines, err
 	}
 
diff --git a/source/memory/memory.go b/source/memory/memory.go
index 2e367d698..87bac0952 100644
--- a/source/memory/memory.go
+++ b/source/memory/memory.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 The Kubernetes Authors.
+Copyright 2018-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,10 +18,11 @@ package memory
 
 import (
 	"io/ioutil"
-	"log"
 	"os"
 	"strings"
 
+	"k8s.io/klog/v2"
+
 	"sigs.k8s.io/node-feature-discovery/source"
 )
 
@@ -47,7 +48,7 @@ func (s Source) Discover() (source.Features, error) {
 	// Detect NUMA
 	numa, err := isNuma()
 	if err != nil {
-		log.Printf("ERROR: failed to detect NUMA topology: %s", err)
+		klog.Errorf("failed to detect NUMA topology: %s", err)
 	} else if numa {
 		features["numa"] = true
 	}
@@ -55,7 +56,7 @@ func (s Source) Discover() (source.Features, error) {
 	// Detect NVDIMM
 	nv, err := detectNvdimm()
 	if err != nil {
-		log.Printf("ERROR: NVDIMM detection failed: %s", err)
+		klog.Errorf("NVDIMM detection failed: %s", err)
 	} else {
 		for k, v := range nv {
 			features["nv."+k] = v
@@ -110,7 +111,7 @@ func detectNvdimm() (map[string]bool, error) {
 			}
 		}
 	} else {
-		log.Printf("WARNING: failed to detect NVDIMM configuration: %s", err)
+		klog.Warningf("failed to detect NVDIMM configuration: %s", err)
 	}
 
 	return features, nil
diff --git a/source/network/network.go b/source/network/network.go
index 4e15b9c73..7baba5eea 100644
--- a/source/network/network.go
+++ b/source/network/network.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2017 The Kubernetes Authors.
+Copyright 2017-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,10 +20,11 @@ import (
 	"bytes"
 	"fmt"
 	"io/ioutil"
-	"log"
 	"strconv"
 	"strings"
 
+	"k8s.io/klog/v2"
+
 	"sigs.k8s.io/node-feature-discovery/source"
 )
 
@@ -66,43 +67,43 @@ func (s Source) Discover() (source.Features, error) {
 		name := netInterface.Name()
 		flags, err := readIfFlags(name)
 		if err != nil {
-			log.Printf("%v", err)
+			klog.Error(err)
 			continue
 		}
 
 		if flags&flagUp != 0 && flags&flagLoopback == 0 {
 			totalBytes, err := ioutil.ReadFile(source.SysfsDir.Path(sysfsBaseDir, name, "device/sriov_totalvfs"))
 			if err != nil {
-				log.Printf("SR-IOV not supported for network interface: %s: %v", name, err)
+				klog.V(1).Infof("SR-IOV not supported for network interface: %s: %v", name, err)
 				continue
 			}
 			total := bytes.TrimSpace(totalBytes)
 			t, err := strconv.Atoi(string(total))
 			if err != nil {
-				log.Printf("Error in obtaining maximum supported number of virtual functions for network interface: %s: %v", name, err)
+				klog.Errorf("Error in obtaining maximum supported number of virtual functions for network interface: %s: %v", name, err)
 				continue
 			}
 			if t > 0 {
-				log.Printf("SR-IOV capability is detected on the network interface: %s", name)
-				log.Printf("%d maximum supported number of virtual functions on network interface: %s", t, name)
+				klog.V(1).Infof("SR-IOV capability is detected on the network interface: %s", name)
+				klog.V(1).Infof("%d maximum supported number of virtual functions on network interface: %s", t, name)
 				features["sriov.capable"] = true
 				numBytes, err := ioutil.ReadFile(source.SysfsDir.Path(sysfsBaseDir, name, "device/sriov_numvfs"))
 				if err != nil {
-					log.Printf("SR-IOV not configured for network interface: %s: %s", name, err)
+					klog.V(1).Infof("SR-IOV not configured for network interface: %s: %s", name, err)
 					continue
 				}
 				num := bytes.TrimSpace(numBytes)
 				n, err := strconv.Atoi(string(num))
 				if err != nil {
-					log.Printf("Error in obtaining the configured number of virtual functions for network interface: %s: %v", name, err)
+					klog.Errorf("Error in obtaining the configured number of virtual functions for network interface: %s: %v", name, err)
 					continue
 				}
 				if n > 0 {
-					log.Printf("%d virtual functions configured on network interface: %s", n, name)
+					klog.V(1).Infof("%d virtual functions configured on network interface: %s", n, name)
 					features["sriov.configured"] = true
 					break
 				} else if n == 0 {
-					log.Printf("SR-IOV not configured on network interface: %s", name)
+					klog.V(1).Infof("SR-IOV not configured on network interface: %s", name)
 				}
 			}
 		}
diff --git a/source/pci/pci.go b/source/pci/pci.go
index d6cf751a6..56b290f13 100644
--- a/source/pci/pci.go
+++ b/source/pci/pci.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 The Kubernetes Authors.
+Copyright 2018-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,9 +18,10 @@ package pci
 
 import (
 	"fmt"
-	"log"
 	"strings"
 
+	"k8s.io/klog/v2"
+
 	"sigs.k8s.io/node-feature-discovery/source"
 	pciutils "sigs.k8s.io/node-feature-discovery/source/internal"
 )
@@ -58,7 +59,7 @@ func (s *Source) SetConfig(conf source.Config) {
 	case *Config:
 		s.config = v
 	default:
-		log.Printf("PANIC: invalid config type: %T", conf)
+		klog.Fatalf("invalid config type: %T", conf)
 	}
 }
 
@@ -84,10 +85,10 @@ func (s Source) Discover() (source.Features, error) {
 		for key := range configLabelFields {
 			keys = append(keys, key)
 		}
-		log.Printf("WARNING: invalid fields '%v' in deviceLabelFields, ignoring...", keys)
+		klog.Warningf("invalid fields '%v' in deviceLabelFields, ignoring...", keys)
 	}
 	if len(deviceLabelFields) == 0 {
-		log.Printf("WARNING: no valid fields in deviceLabelFields defined, using the defaults")
+		klog.Warningf("no valid fields in deviceLabelFields defined, using the defaults")
 		deviceLabelFields = []string{"class", "vendor"}
 	}
 
diff --git a/source/system/system.go b/source/system/system.go
index 6d6819edc..1633dc1cf 100644
--- a/source/system/system.go
+++ b/source/system/system.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 The Kubernetes Authors.
+Copyright 2018-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,11 +18,12 @@ package system
 
 import (
 	"bufio"
-	"log"
 	"os"
 	"regexp"
 	"strings"
 
+	"k8s.io/klog/v2"
+
 	"sigs.k8s.io/node-feature-discovery/source"
 )
 
@@ -50,7 +51,7 @@ func (s Source) Discover() (source.Features, error) {
 
 	release, err := parseOSRelease()
 	if err != nil {
-		log.Printf("ERROR: failed to get os-release: %s", err)
+		klog.Errorf("failed to get os-release: %s", err)
 	} else {
 		for _, key := range osReleaseFields {
 			if value, exists := release[key]; exists {
diff --git a/source/usb/usb.go b/source/usb/usb.go
index 3b50ebabe..2cafcffd0 100644
--- a/source/usb/usb.go
+++ b/source/usb/usb.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2020 The Kubernetes Authors.
+Copyright 2020-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,9 +18,10 @@ package usb
 
 import (
 	"fmt"
-	"log"
 	"strings"
 
+	"k8s.io/klog/v2"
+
 	"sigs.k8s.io/node-feature-discovery/source"
 	usbutils "sigs.k8s.io/node-feature-discovery/source/internal"
 )
@@ -61,7 +62,7 @@ func (s *Source) SetConfig(conf source.Config) {
 	case *Config:
 		s.config = v
 	default:
-		log.Printf("PANIC: invalid config type: %T", conf)
+		klog.Fatalf("invalid config type: %T", conf)
 	}
 }
 
@@ -87,10 +88,10 @@ func (s Source) Discover() (source.Features, error) {
 		for key := range configLabelFields {
 			keys = append(keys, key)
 		}
-		log.Printf("WARNING: invalid fields '%v' in deviceLabelFields, ignoring...", keys)
+		klog.Warningf("invalid fields '%v' in deviceLabelFields, ignoring...", keys)
 	}
 	if len(deviceLabelFields) == 0 {
-		log.Printf("WARNING: no valid fields in deviceLabelFields defined, using the defaults")
+		klog.Warningf("no valid fields in deviceLabelFields defined, using the defaults")
 		deviceLabelFields = []string{"vendor", "device"}
 	}
 


Test patch:
diff --git a/cmd/nfd-worker/main_test.go b/cmd/nfd-worker/main_test.go
index 9481f8116..d36da7b57 100644
--- a/cmd/nfd-worker/main_test.go
+++ b/cmd/nfd-worker/main_test.go
@@ -59,3 +59,19 @@ func TestParseArgs(t *testing.T) {
 		})
 	})
 }
+
+func TestKlogConfigOptName(t *testing.T) {
+	Convey("When converting names of klog command line flags", t, func() {
+		tcs := map[string]string{
+			"":                    "",
+			"a":                   "a",
+			"an_arg":              "anArg",
+			"arg_with_many_parts": "argWithManyParts",
+		}
+		Convey("resulting config option names should be as expected", func() {
+			for input, expected := range tcs {
+				So(klogConfigOptName(input), ShouldEqual, expected)
+			}
+		})
+	})
+}