Repository: NetApp/harvest
Pull request #106 (2021-06-03 03:47:55)

Remove buildmode=plugin
Add support for cluster simulator
WIP Implement Caddy style plugins for collectors
Fix go vet warnings in node.go


Patch:
diff --git a/Makefile b/Makefile
index d9a3c60b..a57536e3 100644
--- a/Makefile
+++ b/Makefile
@@ -20,8 +20,6 @@ BUILD_DATE   := `date +%FT%T%z`
 LD_FLAGS     := "-X 'goharvest2/cmd/harvest/version.VERSION=$(VERSION)' -X 'goharvest2/cmd/harvest/version.Release=$(RELEASE)' -X 'goharvest2/cmd/harvest/version.Commit=$(COMMIT)' -X 'goharvest2/cmd/harvest/version.BuildDate=$(BUILD_DATE)'"
 GOARCH ?= amd64
 GOOS ?= linux
-COLLECTORS := $(shell ls cmd/collectors)
-EXPORTERS := $(shell ls cmd/exporters)
 HARVEST_PACKAGE := harvest-${VERSION}-${RELEASE}_${GOOS}_${GOARCH}
 DIST := dist
 TMP := /tmp/${HARVEST_PACKAGE}
@@ -72,10 +70,10 @@ fmt: ## format the go source files
 	go fmt ./...
 
 vet: ## run go vet on the source files
-	@echo "Running govet"
+	@echo "Running go vet"
 	go vet ./...
 
-build: clean deps fmt harvest collectors exporters ## Build the project
+build: clean deps fmt harvest ## Build the project
 
 package: clean deps build test dist-tar ## Package Harvest binary
 
@@ -90,8 +88,8 @@ harvest: deps
 	@echo "Building poller"
 	@GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o bin/poller -ldflags=$(LD_FLAGS) cmd/poller/poller.go
 
-	@# Build the daemonizer for the pollers
-	@echo "Building daemonizer"
+	@# Build the daemonize for the pollers
+	@echo "Building daemonize"
 	@cd cmd/tools/daemonize; gcc daemonize.c -o ../../../bin/daemonize
 
 	@# Build the zapi tool
@@ -102,44 +100,6 @@ harvest: deps
 	@echo "Building grafana tool"
 	@GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o bin/grafana -ldflags=$(LD_FLAGS) cmd/tools/grafana/main/main.go
 
-###############################################################################
-# Collectors
-###############################################################################
-collectors: deps
-	@echo "Building collectors:"
-	@for collector in ${COLLECTORS}; do                                                   \
-		cd cmd/collectors/$${collector};                                              \
-		echo "  Building $${collector}";                                              \
-		GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags=$(LD_FLAGS) -buildmode=plugin -o ../../../bin/collectors/"$${collector}".so;     \
-		if [ -d plugins ]; then                                                       \
-			echo "    Building plugins for $${collector}";                        \
-	        	cd plugins;                                                           \
-	        	for plugin in `ls`; do                                                \
-				echo "        Building: $${plugin}";                          \
-				cd $${plugin};                                                \
-				GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags=$(LD_FLAGS) -buildmode=plugin -o ../../../../../bin/plugins/"$${collector}"/"$${plugin}".so; \
-				cd ../;                                                       \
-			done;                                                                 \
-			cd ../../../../;                                                      \
-		else                                                                          \
-	       		cd - > /dev/null;                                                     \
-		fi;                                                                           \
-	done
-
-###############################################################################
-# Exporters
-###############################################################################
-exporters: deps
-	@echo "Building exporters:"
-	@for exporter in ${EXPORTERS}; do                                                     \
-		cd cmd/exporters/$${exporter};                                                \
-		echo "  Building $${exporter}";                                               \
-		GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags=$(LD_FLAGS) -buildmode=plugin -o ../../../bin/exporters/"$${exporter}".so;       \
-	       	cd - > /dev/null;                                                             \
-	done
-
-
-
 ###############################################################################
 # Build tar gz distribution
 ###############################################################################
diff --git a/cmd/collectors/unix/errors.go b/cmd/collectors/unix/errors.go
index 46b9fb20..2e296b98 100644
--- a/cmd/collectors/unix/errors.go
+++ b/cmd/collectors/unix/errors.go
@@ -2,7 +2,7 @@
  * Copyright NetApp Inc, 2021 All rights reserved
  */
 
-package main
+package unix
 
 const (
 	PROCESS_NOT_FOUND = "process not found"
diff --git a/cmd/collectors/unix/main.go b/cmd/collectors/unix/main.go
index 7bb32175..155408db 100644
--- a/cmd/collectors/unix/main.go
+++ b/cmd/collectors/unix/main.go
@@ -1,10 +1,11 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package unix
 
 import (
 	"goharvest2/cmd/poller/collector"
+	"goharvest2/cmd/poller/plugin"
 	"goharvest2/pkg/conf"
 	"goharvest2/pkg/errors"
 	"goharvest2/pkg/logging"
@@ -25,7 +26,7 @@ import (
 // https://en.wikipedia.org/wiki/Procfs
 var _SUPPORTED_PLATFORMS = []string{
 	"aix",
-	"andriod", // available in termux
+	"android", // available in termux
 	"dragonfly",
 	"freebsd", // available, but not mounted by default
 	"linux",
@@ -70,6 +71,17 @@ var _DTYPES = map[string]string{
 	"fds":            "uint64",
 }
 
+func init() {
+	plugin.RegisterModule(Unix{})
+}
+
+func (Unix) HarvestModule() plugin.ModuleInfo {
+	return plugin.ModuleInfo{
+		ID:  "harvest.collector.unix",
+		New: func() plugin.Module { return new(Unix) },
+	}
+}
+
 func getClockTicks() {
 	_CLK_TCK = 100.0
 	if data, err := exec.Command("getconf", "CLK_TCK").Output(); err == nil {
@@ -127,14 +139,9 @@ type Unix struct {
 	processes       map[string]*Process
 }
 
-// New - create new, uninitialized collector
-func New(a *collector.AbstractCollector) collector.Collector {
-	return &Unix{AbstractCollector: a}
-}
-
 // Init - initialize the collector
-func (me *Unix) Init() error {
-
+func (me *Unix) Init(a *collector.AbstractCollector) error {
+	me.AbstractCollector = a
 	var err error
 
 	if !set.NewFrom(_SUPPORTED_PLATFORMS).Has(runtime.GOOS) {
@@ -150,7 +157,7 @@ func (me *Unix) Init() error {
 		_MOUNT_POINT = mp
 	}
 
-	// assert fs is avilable
+	// assert fs is available
 	if fi, err := os.Stat(_MOUNT_POINT); err != nil || !fi.IsDir() {
 		return errors.New(errors.ERR_IMPLEMENT, "filesystem ["+_MOUNT_POINT+"] not available")
 	}
@@ -416,14 +423,14 @@ func setStartTime(m matrix.Metric, i *matrix.Instance, p *Process, s *System) {
 	}
 }
 
-func setNumThreads(m matrix.Metric, i *matrix.Instance, p *Process, s *System) {
+func setNumThreads(m matrix.Metric, i *matrix.Instance, p *Process, _ *System) {
 	err := m.SetValueUint64(i, p.numThreads)
 	if err != nil {
 		logging.Get().Error().Stack().Err(err).Msg("error")
 	}
 }
 
-func setNumFds(m matrix.Metric, i *matrix.Instance, p *Process, s *System) {
+func setNumFds(m matrix.Metric, i *matrix.Instance, p *Process, _ *System) {
 	err := m.SetValueUint64(i, p.numFds)
 	if err != nil {
 		logging.Get().Error().Stack().Err(err).Msg("error")
@@ -437,7 +444,7 @@ func setMemoryPercent(m matrix.Metric, i *matrix.Instance, p *Process, s *System
 	}
 }
 
-func setCpuPercent(m matrix.Metric, i *matrix.Instance, p *Process, s *System) {
+func setCpuPercent(m matrix.Metric, i *matrix.Instance, p *Process, _ *System) {
 	if p.elapsedTime != 0 {
 		err := m.SetValueFloat64(i, p.cpuTotal/p.elapsedTime*100)
 		if err != nil {
@@ -496,5 +503,7 @@ func setCtx(m matrix.Metric, l string, i *matrix.Instance, p *Process) {
 	}
 }
 
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
+// Interface guards
+var (
+	_ collector.Collector = (*Unix)(nil)
+)
diff --git a/cmd/collectors/unix/process.go b/cmd/collectors/unix/process.go
index 2755edd5..c4178b11 100644
--- a/cmd/collectors/unix/process.go
+++ b/cmd/collectors/unix/process.go
@@ -2,7 +2,7 @@
  * Copyright NetApp Inc, 2021 All rights reserved
  */
 
-package main
+package unix
 
 import (
 	"bytes"
diff --git a/cmd/collectors/unix/system.go b/cmd/collectors/unix/system.go
index aa123eee..b55459d1 100644
--- a/cmd/collectors/unix/system.go
+++ b/cmd/collectors/unix/system.go
@@ -2,7 +2,7 @@
  * Copyright NetApp Inc, 2021 All rights reserved
  */
 
-package main
+package unix
 
 import (
 	"goharvest2/pkg/errors"
diff --git a/cmd/collectors/zapi/collector/zapi.go b/cmd/collectors/zapi/collector/zapi.go
index 29f8e5a8..37419449 100644
--- a/cmd/collectors/zapi/collector/zapi.go
+++ b/cmd/collectors/zapi/collector/zapi.go
@@ -4,6 +4,13 @@
 package zapi
 
 import (
+	"goharvest2/cmd/collectors/zapi/plugins/shelf"
+	"goharvest2/cmd/collectors/zapi/plugins/snapmirror"
+	"goharvest2/cmd/collectors/zapiperf/plugins/fcp"
+	"goharvest2/cmd/collectors/zapiperf/plugins/headroom"
+	"goharvest2/cmd/collectors/zapiperf/plugins/nic"
+	"goharvest2/cmd/collectors/zapiperf/plugins/volume"
+	"goharvest2/cmd/poller/plugin"
 	"strconv"
 	"strings"
 	"time"
@@ -33,16 +40,19 @@ type Zapi struct {
 	shortestPathPrefix []string
 }
 
-func New(a *collector.AbstractCollector) collector.Collector {
-	return &Zapi{AbstractCollector: a}
+func init() {
+	plugin.RegisterModule(Zapi{})
 }
 
-func NewZapi(a *collector.AbstractCollector) *Zapi {
-	return &Zapi{AbstractCollector: a}
+func (Zapi) HarvestModule() plugin.ModuleInfo {
+	return plugin.ModuleInfo{
+		ID:  "harvest.collector.zapi",
+		New: func() plugin.Module { return new(Zapi) },
+	}
 }
 
-func (me *Zapi) Init() error {
-
+func (me *Zapi) Init(a *collector.AbstractCollector) error {
+	me.AbstractCollector = a
 	if err := me.InitVars(); err != nil {
 		return err
 	}
@@ -103,6 +113,26 @@ func (me *Zapi) InitVars() error {
 	return nil
 }
 
+func (me *Zapi) LoadPlugin(kind string, abc *plugin.AbstractPlugin) plugin.Plugin {
+	switch kind {
+	case "Snapmirror":
+		return snapmirror.New(abc)
+	case "Shelf":
+		return shelf.New(abc)
+	case "Nic":
+		return nic.New(abc)
+	case "Fcp":
+		return fcp.New(abc)
+	case "Headroom":
+		return headroom.New(abc)
+	case "Volume":
+		return volume.New(abc)
+	default:
+		me.Logger.Info().Msgf("no zapi plugin found for %s", kind)
+	}
+	return nil
+}
+
 func (me *Zapi) InitCache() error {
 
 	if me.Client.IsClustered() {
@@ -375,3 +405,8 @@ func (me *Zapi) PollData() (*matrix.Matrix, error) {
 
 	return me.Matrix, nil
 }
+
+// Interface guards
+var (
+	_ collector.Collector = (*Zapi)(nil)
+)
diff --git a/cmd/collectors/zapi/main.go b/cmd/collectors/zapi/main.go
deleted file mode 100644
index 5f9b3834..00000000
--- a/cmd/collectors/zapi/main.go
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Copyright NetApp Inc, 2021 All rights reserved
- */
-package main
-
-import (
-	zapi "goharvest2/cmd/collectors/zapi/collector"
-	"goharvest2/cmd/poller/collector"
-)
-
-func New(a *collector.AbstractCollector) collector.Collector {
-	return zapi.New(a)
-}
-
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
diff --git a/cmd/collectors/zapi/plugins/shelf/shelf.go b/cmd/collectors/zapi/plugins/shelf/shelf.go
index a1d23e0b..11477671 100644
--- a/cmd/collectors/zapi/plugins/shelf/shelf.go
+++ b/cmd/collectors/zapi/plugins/shelf/shelf.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package shelf
 
 import (
 	"goharvest2/cmd/poller/collector"
@@ -260,6 +260,3 @@ func (my *Shelf) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
 
 	return output, nil
 }
-
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
diff --git a/cmd/collectors/zapi/plugins/snapmirror/snapmirror.go b/cmd/collectors/zapi/plugins/snapmirror/snapmirror.go
index f10d3179..7141fde6 100644
--- a/cmd/collectors/zapi/plugins/snapmirror/snapmirror.go
+++ b/cmd/collectors/zapi/plugins/snapmirror/snapmirror.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package snapmirror
 
 import (
 	"goharvest2/cmd/poller/plugin"
@@ -230,8 +230,4 @@ func (my *SnapMirror) updateLimitCache() error {
 	}
 	my.Logger.Debug().Msgf("updated limit cache for %d nodes", count)
 	return nil
-
 }
-
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
diff --git a/cmd/collectors/zapiperf/plugins/fcp/fcp.go b/cmd/collectors/zapiperf/plugins/fcp/fcp.go
index 4a81e133..442cafb7 100644
--- a/cmd/collectors/zapiperf/plugins/fcp/fcp.go
+++ b/cmd/collectors/zapiperf/plugins/fcp/fcp.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package fcp
 
 import (
 	"goharvest2/cmd/poller/plugin"
@@ -100,6 +100,3 @@ func (me *Fcp) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
 	}
 	return nil, nil
 }
-
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
diff --git a/cmd/collectors/zapiperf/plugins/headroom/headroom.go b/cmd/collectors/zapiperf/plugins/headroom/headroom.go
index 32ab5a2c..370c4679 100644
--- a/cmd/collectors/zapiperf/plugins/headroom/headroom.go
+++ b/cmd/collectors/zapiperf/plugins/headroom/headroom.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package headroom
 
 import (
 	"goharvest2/cmd/poller/plugin"
@@ -40,6 +40,3 @@ func (me *Headroom) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
 
 	return nil, nil
 }
-
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
diff --git a/cmd/collectors/zapiperf/plugins/nic/nic.go b/cmd/collectors/zapiperf/plugins/nic/nic.go
index 60abc821..25b7e5db 100644
--- a/cmd/collectors/zapiperf/plugins/nic/nic.go
+++ b/cmd/collectors/zapiperf/plugins/nic/nic.go
@@ -11,7 +11,7 @@ Package Description:
 		  - "nic_state":     0 if port is up, 1 otherwise
 
 */
-package main
+package nic
 
 import (
 	"goharvest2/cmd/poller/plugin"
@@ -124,6 +124,3 @@ func (me *Nic) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
 
 	return nil, nil
 }
-
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
diff --git a/cmd/collectors/zapiperf/plugins/volume/volume.go b/cmd/collectors/zapiperf/plugins/volume/volume.go
index 53fc8c2d..54c0dd16 100644
--- a/cmd/collectors/zapiperf/plugins/volume/volume.go
+++ b/cmd/collectors/zapiperf/plugins/volume/volume.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package volume
 
 import (
 	"goharvest2/cmd/poller/plugin"
@@ -150,6 +150,3 @@ func (me *Volume) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
 
 	return []*matrix.Matrix{cache}, nil
 }
-
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
diff --git a/cmd/collectors/zapiperf/zapiperf.go b/cmd/collectors/zapiperf/zapiperf.go
index 759b104c..48d29259 100644
--- a/cmd/collectors/zapiperf/zapiperf.go
+++ b/cmd/collectors/zapiperf/zapiperf.go
@@ -1,30 +1,11 @@
 /*
-	Copyright NetApp Inc, 2021 All rights reserved
-
-	ZapiPerf collects and processes metrics from the "perf" APIs of the
-	ZAPI protocol. This collector inherits some methods and fields of
-	the Zapi collector (as they use the same protocol). However,
-	ZapiPerf calculates final metric values from the deltas of two
-	consecutive polls.
-
-	The exact formula of doing these calculations, depends on the property
-	of each counter and some counters require a "base-counter" additionally.
-
-	Counter properties and other metadata are fetched from the target system
-	during PollCounter() making the collector ONTAP-version independent.
-
-	The collector maintains a cache of instances, updated periodically as well,
-	during PollInstance().
-
-	The source code prioritizes performance over simplicity/readability.
-	Additionally, some objects (e.g. workloads) come with twists that
-	force the collector to do acrobatics. Don't expect to easily
-	comprehend what comes below.
-*/
-package main
+ * Copyright NetApp Inc, 2021 All rights reserved
+ */
+package zapiperf
 
 import (
 	"goharvest2/cmd/poller/collector"
+	"goharvest2/cmd/poller/plugin"
 	"goharvest2/pkg/color"
 	"goharvest2/pkg/dict"
 	"goharvest2/pkg/errors"
@@ -38,45 +19,45 @@ import (
 	zapi "goharvest2/cmd/collectors/zapi/collector"
 )
 
+// default parameter values
 const (
-	// default parameter values
 	instanceKey   = "uuid"
 	batchSize     = 500
 	latencyIoReqd = 10
-	// objects that need special handling
-	objWorkload             = "workload"
-	objWorkloadDetail       = "workload_detail"
-	objWorkloadVolume       = "workload_volume"
-	objWorkloadDetailVolume = "workload_detail_volume"
 )
 
 const BILLION = 1000000000
 
 type ZapiPerf struct {
-	*zapi.Zapi      // provides: AbstractCollector, Client, Object, Query, TemplateFn, TemplateType
+	*zapi.Zapi      // provides: AbstractCollector, Connection, Object, Query, TemplateFn, TemplateType
 	object          string
 	batchSize       int
 	latencyIoReqd   int
 	instanceKey     string
 	instanceLabels  map[string]string
 	histogramLabels map[string][]string
-	qosLabels       map[string]string
 	isCacheEmpty    bool
 }
 
-// New returns a new ZapiPerf collector for one perf object
-func New(a *collector.AbstractCollector) collector.Collector {
-	return &ZapiPerf{Zapi: zapi.NewZapi(a)}
+func init() {
+	plugin.RegisterModule(ZapiPerf{})
 }
 
-// Init initializes collector parameters and connects to target system
-func (me *ZapiPerf) Init() error {
+func (ZapiPerf) HarvestModule() plugin.ModuleInfo {
+	return plugin.ModuleInfo{
+		ID:  "harvest.collector.zapiperf",
+		New: func() plugin.Module { return new(ZapiPerf) },
+	}
+}
+
+func (me *ZapiPerf) Init(a *collector.AbstractCollector) error {
+	me.Zapi = &zapi.Zapi{AbstractCollector: a}
 
 	if err := me.InitVars(); err != nil {
 		return err
 	}
 	// Invoke generic initializer
-	// this will load Schedule, initialize data and metadata Matrices
+	// this will load Schedule, initialize Data and Metadata
 	if err := collector.Init(me); err != nil {
 		return err
 	}
@@ -111,7 +92,6 @@ func (me *ZapiPerf) InitCache() error {
 	return nil
 }
 
-// load a string parameter or use defaultValue
 func (me *ZapiPerf) loadParamStr(name, defaultValue string) string {
 
 	var x string
@@ -124,14 +104,11 @@ func (me *ZapiPerf) loadParamStr(name, defaultValue string) string {
 	return defaultValue
 }
 
-// load an int parameter or use defaultValue
 func (me *ZapiPerf) loadParamInt(name string, defaultValue int) int {
 
-	var (
-		x string
-		n int
-		e error
-	)
+	var x string
+	var n int
+	var e error
 
 	if x = me.Params.GetChildContentS(name); x != "" {
 		if n, e = strconv.Atoi(x); e == nil {
@@ -145,15 +122,9 @@ func (me *ZapiPerf) loadParamInt(name string, defaultValue int) int {
 	return defaultValue
 }
 
-// PollData updates the data cache of the collector. During first poll, no data will
-// be emitted. Afterwards, final metric values will be calculated from previous poll.
 func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 
-	var (
-		instanceKeys    []string
-		resourceLatency matrix.Metric // for workload* objects
-		err             error
-	)
+	var err error
 
 	me.Logger.Debug().Msg("updating data cache")
 
@@ -180,20 +151,7 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 
 	// list of instance keys (instance names or uuids) for which
 	// we will request counter data
-	if me.Query == objWorkloadDetail || me.Query == objWorkloadDetailVolume {
-		if resourceMap := me.Params.GetChildS("resource_map"); resourceMap == nil {
-			return nil, errors.New(errors.MISSING_PARAM, "resource_map")
-		} else {
-			instanceKeys = make([]string, 0)
-			for _, layer := range resourceMap.GetAllChildNamesS() {
-				for key := range me.Matrix.GetInstances() {
-					instanceKeys = append(instanceKeys, key+"."+layer)
-				}
-			}
-		}
-	} else {
-		instanceKeys = newData.GetInstanceKeys()
-	}
+	instanceKeys := newData.GetInstanceKeys()
 
 	// build ZAPI request
 	request := node.NewXmlS("perf-object-get-instances")
@@ -203,7 +161,7 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 	requestCounters := request.NewChildS("counters", "")
 	// load scalar metrics
 	for key, m := range newData.GetMetrics() {
-		// skip histograms
+		// no histograms
 		if !m.HasLabels() {
 			requestCounters.NewChildS("counter", key)
 		}
@@ -241,18 +199,15 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 
 		if err = me.Client.BuildRequest(request); err != nil {
 			me.Logger.Error().Stack().Err(err).Msg("build request: ")
+			//break?
 			return nil, err
 		}
 
 		response, rd, pd, err := me.Client.InvokeWithTimers()
 		if err != nil {
-			// if ONTAP complains about batch size, use a smaller batch size
-			if strings.Contains(err.Error(), "resource limit exceeded") && me.batchSize > 100 {
-				me.Logger.Error().Stack().Err(err)
-				me.Logger.Info().Msgf("changed batch_size from [%d] to [%d]", me.batchSize, me.batchSize-100)
-				me.batchSize -= 100
-				return nil, nil
-			}
+			//me.Logger.Error(me.Prefix, "data request: %v", err)
+			//@TODO handle "resource limit exceeded"
+			//break
 			return nil, err
 		}
 
@@ -277,27 +232,6 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 		for _, i := range instances.GetChildren() {
 
 			key := i.GetChildContentS(me.instanceKey)
-
-			// special case for these two objects
-			// we need to process each latency layer for each instance/counter
-			if me.Query == objWorkloadDetail || me.Query == objWorkloadDetailVolume {
-
-				layer := "" // latency layer (resource) for workloads
-
-				if x := strings.Split(key, "."); len(x) == 2 {
-					key = x[0]
-					layer = x[1]
-				} else {
-					me.Logger.Warn().Msgf("instance name [%s] has unexpected format", key)
-					continue
-				}
-
-				if resourceLatency = newData.GetMetric(layer); resourceLatency == nil {
-					me.Logger.Warn().Msgf("resource-latency metric [%s] missing in cache", layer)
-					continue
-				}
-			}
-
 			if key == "" {
 				me.Logger.Debug().Msgf("skip instance, no key [%s] (name=%s, uuid=%s)", me.instanceKey, i.GetChildContentS("name"), i.GetChildContentS("uuid"))
 				continue
@@ -318,8 +252,9 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 			me.Logger.Debug().Msgf("fetching data of instance [%s]", key)
 
 			// add batch timestamp as custom counter
-			if err := timestamp.SetValueFloat64(instance, ts); err != nil {
-				me.Logger.Error().Stack().Err(err).Msg("set timestamp value: ")
+			err := timestamp.SetValueFloat64(instance, ts)
+			if err != nil {
+				me.Logger.Error().Stack().Err(err).Msg("")
 			}
 
 			for _, cnt := range counters.GetChildren() {
@@ -337,7 +272,7 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 				me.Logger.Trace().Msgf("(%s%s%s) parsing counter (%s) = %v", color.Grey, key, color.End, name, value)
 
 				// ZAPI counter for us is either instance label (string)
-				// or numeric metric (scalar or histogram)
+				// or numeric metric (scalar or string)
 
 				// store as instance label
 				if display, has := me.instanceLabels[name]; has {
@@ -372,23 +307,6 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 					continue
 				}
 
-				// special case for workload_detail
-				if me.Query == objWorkloadDetail || me.Query == objWorkloadDetailVolume {
-					if name == "wait_time" || name == "service_time" {
-						if err := resourceLatency.AddValueString(instance, value); err != nil {
-							me.Logger.Error().Stack().Err(err).Msgf("add resource-latency (%s) value [%s]: %v", name, value, err)
-						} else {
-							me.Logger.Trace().Msgf("++ resource-latency (%s) = [%s%s%s]", name, color.Blue, value, color.End)
-							count++
-						}
-						continue
-					}
-					// "visits" are ignored
-					if name == "visits" {
-						continue
-					}
-				}
-
 				// store as scalar metric
 				if metric := newData.GetMetric(name); metric != nil {
 					if err = metric.SetValueString(instance, value); err != nil {
@@ -407,17 +325,8 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 		} // end loop over instances
 	} // end batch request
 
-	me.Logger.Debug().Msgf("collected %d data points in %d batch polls", count, batchCount)
-
-	if me.Query == objWorkloadDetail || me.Query == objWorkloadDetailVolume {
-		if rd, pd, err := me.getParentOpsCounters(newData, keyName); err == nil {
-			apiT += rd
-			parseT += pd
-		} else {
-			// no point to continue as we can't calculate the other counters
-			return nil, err
-		}
-	}
+	// terminate if serious errors
+	// @TODO handle...
 
 	// update metadata
 	me.Metadata.LazySetValueInt64("api_time", "data", apiT.Microseconds())
@@ -425,6 +334,8 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 	me.Metadata.LazySetValueUint64("count", "data", count)
 	me.AddCollectCount(count)
 
+	me.Logger.Debug().Msgf("collected %d data points in %d batch polls", count, batchCount)
+
 	// skip calculating from delta if no data from previous poll
 	if me.isCacheEmpty {
 		me.Logger.Debug().Msg("skip postprocessing until next poll (previous cache empty)")
@@ -436,8 +347,9 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 	calcStart := time.Now()
 
 	me.Logger.Debug().Msg("starting delta calculations from previous cache")
+	//me.Logger.Debug(me.Prefix, "data has dimensions (%d x %d)", len(newData.Data), len(newData.Data[0]))
 
-	// cache raw data for next poll
+	// cache data, to store after calculations
 	cachedData := newData.Clone(true, true, true) // @TODO implement copy data
 
 	// order metrics, such that those requiring base counters are processed last
@@ -459,11 +371,18 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 
 	// calculate timestamp delta first since many counters require it for postprocessing
 	// timestamp has "raw" property, so won't be postprocessed automatically
+	// fmt.Printf("\npostprocessing %s%s%s - %s%v%s\n", color.Red, timestamp.Name, color.End, color.Bold, timestamp.Properties, color.End)
+	//me.Logger.Debug(me.Prefix, "cooking [%s] (%s)", timestamp.Name, timestamp.Properties)
+	//print_vector("current", newData.Data[timestamp.Index])
+	//print_vector("previous", me.Data.Data[timestamp.Index])
 	if err = timestamp.Delta(me.Matrix.GetMetric("timestamp")); err != nil {
 		me.Logger.Error().Stack().Err(err).Msg("(timestamp) calculate delta:")
 		// @TODO terminate since other counters will be incorrect
 	}
 
+	//newData.Delta(me.Data, timestamp.Index)
+	//print_vector(color.Green+"delta"+color.End, newData.Data[timestamp.Index])
+
 	var base matrix.Metric
 
 	for i, metric := range orderedMetrics {
@@ -471,28 +390,25 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 		property := metric.GetProperty()
 		key := orderedKeys[i]
 
-		// RAW - submit without posprocessing
-		if property == "raw" {
+		// raw counters don't require postprocessing
+		if property == "raw" || property == "" {
 			continue
 		}
 
-		// all other properties - first calculate delta
+		// for all the other properties we start with delta
 		if err = metric.Delta(me.Matrix.GetMetric(key)); err != nil {
 			me.Logger.Error().Stack().Err(err).Msgf("(%s) calculate delta:", key)
 			continue
 		}
 
-		// DELTA - subtract previous value from current
 		if property == "delta" {
 			// already done
 			continue
 		}
 
-		// RATE - delta, normalized by elapsed time
+		// rate is delta, normalized by elapsed time
+		// we skip calculating rates to fist calculate averages and percentages
 		if property == "rate" {
-			// defer calculation, so we can first calculate averages/percents
-			// Note: calculating rate before averages are averages/percentages are calculated
-			// used to be a bug in Harvest 2.0 (Alpha, RC1, RC2) resulting in very high latency values
 			continue
 		}
 
@@ -504,15 +420,11 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 			continue
 		}
 
-		// remaining properties: average and percent
-		//
-		// AVERAGE - delta, divided by base-counter delta
-		//
-		// PERCENT - average * 100
+		// average and percentage are calculated by dividing by the value of the base counter
 		// special case for latency counter: apply minimum number of iops as threshold
 		if property == "average" || property == "percent" {
 
-			if strings.HasSuffix(metric.GetName(), "latency") {
+			if strings.HasSuffix(metric.GetName(), "_latency") {
 				err = metric.DivideWithThreshold(base, me.latencyIoReqd)
 			} else {
 				err = metric.Divide(base)
@@ -547,139 +459,13 @@ func (me *ZapiPerf) PollData() (*matrix.Matrix, error) {
 	}
 
 	me.Metadata.LazySetValueInt64("calc_time", "data", time.Since(calcStart).Microseconds())
-
 	// store cache for next poll
 	me.Matrix = cachedData
+	//me.Data.IsEmpty = false // @redundant
 
 	return newData, nil
 }
 
-// Poll counter "ops" of the rlated/parent object, required for objects
-// workload_detail and workload_detail_volume. This counter is already
-// collected by the other ZapiPerf collectors, so this poll is redundant
-// (until we implement some sort of inter-collector communication).
-func (me *ZapiPerf) getParentOpsCounters(data *matrix.Matrix, KeyAttr string) (time.Duration, time.Duration, error) {
-
-	var (
-		ops          matrix.Metric
-		object       string
-		instanceKeys []string
-		apiT, parseT time.Duration
-		err          error
-	)
-
-	if me.Query == objWorkloadDetail {
-		object = objWorkload
-	} else {
-		object = objWorkloadVolume
-	}
-
-	me.Logger.Debug().Msgf("(%s) starting redundancy poll for ops from parent object (%s)", me.Query, object)
-
-	apiT = 0 * time.Second
-	parseT = 0 * time.Second
-
-	if ops = data.GetMetric("ops"); ops == nil {
-		me.Logger.Error().Stack().Err(nil).Msgf("ops counter not found in cache")
-		return apiT, parseT, errors.New(errors.MISSING_PARAM, "counter ops")
-	}
-
-	instanceKeys = data.GetInstanceKeys()
-
-	// build ZAPI request
-	request := node.NewXmlS("perf-object-get-instances")
-	request.NewChildS("objectname", object)
-
-	requestCounters := request.NewChildS("counters", "")
-	requestCounters.NewChildS("counter", "ops")
-
-	// batch indices
-	startIndex := 0
-	endIndex := 0
-
-	count := 0
-
-	for endIndex < len(instanceKeys) {
-
-		// update batch indices
-		endIndex += me.batchSize
-		if endIndex > len(instanceKeys) {
-			endIndex = len(instanceKeys)
-		}
-
-		me.Logger.Debug().Msgf("starting batch poll for instances [%d:%d]", startIndex, endIndex)
-
-		request.PopChildS(KeyAttr + "s")
-		requestInstances := request.NewChildS(KeyAttr+"s", "")
-		for _, key := range instanceKeys[startIndex:endIndex] {
-			requestInstances.NewChildS(KeyAttr, key)
-		}
-
-		startIndex = endIndex
-
-		if err = me.Client.BuildRequest(request); err != nil {
-			return apiT, parseT, err
-		}
-
-		response, rt, pt, err := me.Client.InvokeWithTimers()
-		if err != nil {
-			return apiT, parseT, err
-		}
-
-		apiT += rt
-		parseT += pt
-
-		// fetch instances
-		instances := response.GetChildS("instances")
-		if instances == nil || len(instances.GetChildren()) == 0 {
-			return apiT, parseT, err
-		}
-
-		for _, i := range instances.GetChildren() {
-
-			key := i.GetChildContentS(me.instanceKey)
-
-			if key == "" {
-				me.Logger.Debug().Msgf("skip instance, no key [%s] (name=%s, uuid=%s)", me.instanceKey, i.GetChildContentS("name"), i.GetChildContentS("uuid"))
-				continue
-			}
-
-			instance := data.GetInstance(key)
-			if instance == nil {
-				me.Logger.Warn().Msgf("skip instance [%s], not found in cache", key)
-				continue
-			}
-
-			counters := i.GetChildS("counters")
-			if counters == nil {
-				me.Logger.Debug().Msgf("skip instance [%s], no data counters", key)
-				continue
-			}
-
-			for _, cnt := range counters.GetChildren() {
-
-				name := cnt.GetChildContentS("name")
-				value := cnt.GetChildContentS("value")
-
-				me.Logger.Trace().Msgf("(%s%s%s%s) parsing counter = %v", color.Grey, key, color.End, name, value)
-
-				if name == "ops" {
-					if err = ops.SetValueString(instance, value); err != nil {
-						me.Logger.Error().Stack().Err(err).Msgf("set metric (%s) value [%s]", name, value)
-					} else {
-						me.Logger.Trace().Msgf("+ metric (%s) = [%s%s%s]", name, color.Cyan, value, color.End)
-						count++
-					}
-				} else {
-					me.Logger.Error().Stack().Err(nil).Msgf("unrequested metric (%s)", name)
-				}
-			}
-		}
-	}
-	me.Logger.Debug().Msgf("(%s) completed redundant ops poll (%s): collected %d", me.Query, object, count)
-	return apiT, parseT, nil
-}
-
 func (me *ZapiPerf) PollCounter() (*matrix.Matrix, error) {
 
 	var (
@@ -757,7 +543,6 @@ func (me *ZapiPerf) PollCounter() (*matrix.Matrix, error) {
 
 		// override counter properties from template
 		if p := me.GetOverride(key); p != "" {
-			me.Logger.Debug().Msgf("%soverride counter [%s] property [%s] => [%s]%s", color.Red, key, counter.GetChildContentS("properties"), p, color.End)
 			counter.SetChildContentS("properties", p)
 		}
 
@@ -802,7 +587,7 @@ func (me *ZapiPerf) PollCounter() (*matrix.Matrix, error) {
 		for name, counter := range counters {
 			if replaced.Has(name) {
 				oldMetrics.Delete(name)
-				me.Logger.Debug().Msgf("adding [%s] (replacment for deprecated counter)", name)
+				me.Logger.Debug().Msgf("adding [%s] (replacement for deprecated counter)", name)
 				if r := me.addCounter(counter, name, name, true, counters); r != "" && !wanted.Has(r) {
 					missing.Add(r) // required base counter, missing in template
 					me.Logger.Debug().Msgf("%smarking [%s] as required base counter for [%s]%s", color.Red, r, name, color.End)
@@ -815,6 +600,7 @@ func (me *ZapiPerf) PollCounter() (*matrix.Matrix, error) {
 	if missing.Size() > 0 {
 		me.Logger.Debug().Msgf("attempting to retrieve metadata of %d missing base counters", missing.Size())
 		for name, counter := range counters {
+			//me.Logger.Debug(me.Prefix, "%shas??? [%s]%s", color.Grey, name, color.End)
 			if missing.Has(name) {
 				oldMetrics.Delete(name)
 				me.Logger.Debug().Msgf("adding [%s] (missing base counter)", name)
@@ -836,88 +622,6 @@ func (me *ZapiPerf) PollCounter() (*matrix.Matrix, error) {
 		m.SetExportable(false)
 	}
 
-	// hack for workload objects, @TODO replace with a plugin
-	if me.Query == objWorkload || me.Query == objWorkloadDetail || me.Query == objWorkloadVolume || me.Query == objWorkloadDetailVolume {
-
-		// for these two objects, we need to create latency/ops counters for each of the workload layers
-		// there original counters will be discarded
-		if me.Query == objWorkloadDetail || me.Query == objWorkloadDetailVolume {
-
-			var service, wait, visits, ops matrix.Metric
-			oldMetrics.Delete("service_time")
-			oldMetrics.Delete("wait_time")
-			oldMetrics.Delete("visits")
-			oldMetrics.Delete("ops")
-
-			if service = me.Matrix.GetMetric("service_time"); service == nil {
-				me.Logger.Error().Stack().Err(nil).Msg("metric [service_time] required to calculate workload missing")
-			}
-
-			if wait = me.Matrix.GetMetric("wait_time"); wait == nil {
-				me.Logger.Error().Stack().Err(nil).Msg("metric [wait-time] required to calculate workload missing")
-			}
-
-			if visits = me.Matrix.GetMetric("visits"); visits == nil {
-				me.Logger.Error().Stack().Err(nil).Msg("metric [visits] required to calculate workload missing")
-			}
-
-			if service == nil || wait == nil || visits == nil {
-				return nil, errors.New(errors.MISSING_PARAM, "workload metrics")
-			}
-
-			if ops = me.Matrix.GetMetric("ops"); ops == nil {
-				if ops, err = me.Matrix.NewMetricFloat64("ops"); err != nil {
-					return nil, err
-				}
-				ops.SetProperty(visits.GetProperty())
-				me.Logger.Debug().Msgf("+ [resource_ops] [%s] added workload ops metric with property (%s)", ops.GetName(), ops.GetProperty())
-			}
-
-			service.SetExportable(false)
-			wait.SetExportable(false)
-			visits.SetExportable(false)
-
-			if resourceMap := me.Params.GetChildS("resource_map"); resourceMap == nil {
-				return nil, errors.New(errors.MISSING_PARAM, "resource_map")
-			} else {
-				for _, x := range resourceMap.GetChildren() {
-					name := x.GetNameS()
-					resource := x.GetContentS()
-
-					if m, err := me.Matrix.NewMetricFloat64(name); err != nil {
-						return nil, err
-					} else {
-						m.SetName("resource_latency")
-						m.SetLabel("resource", resource)
-						m.SetProperty(service.GetProperty())
-						// base counter is the ops of the same resource
-						m.SetComment("ops")
-
-						oldMetrics.Delete(name)
-						me.Logger.Debug().Msgf("+ [%s] (=> %s) added workload latency metric", name, resource)
-					}
-				}
-			}
-		}
-
-		if qosLabels := me.Params.GetChildS("qos_labels"); qosLabels == nil {
-			return nil, errors.New(errors.MISSING_PARAM, "qos_labels")
-		} else {
-			me.qosLabels = make(map[string]string)
-			for _, label := range qosLabels.GetAllChildContentS() {
-
-				display := strings.ReplaceAll(label, "-", "_")
-				if x := strings.Split(label, "=>"); len(x) == 2 {
-					label = strings.TrimSpace(x[0])
-					display = strings.TrimSpace(x[1])
-				}
-				me.qosLabels[label] = display
-				//me.instanceLabels[label] = display
-				//oldLabels.Delete(label)
-			}
-		}
-	}
-
 	for key := range oldMetrics.Iter() {
 		// temporary fix: prevent removing array counters
 		// @TODO
@@ -928,6 +632,7 @@ func (me *ZapiPerf) PollCounter() (*matrix.Matrix, error) {
 	}
 
 	for key := range oldLabels.Iter() {
+		//me.Data.RemoveLabel(key)
 		delete(me.instanceLabels, key)
 		me.Logger.Debug().Msgf("removed label [%s]", key)
 	}
@@ -945,7 +650,6 @@ func (me *ZapiPerf) PollCounter() (*matrix.Matrix, error) {
 	return nil, nil
 }
 
-// create new or update existing metric based on Zapi counter metadata
 func (me *ZapiPerf) addCounter(counter *node.Node, name, display string, enabled bool, cache map[string]*node.Node) string {
 
 	var (
@@ -1062,7 +766,6 @@ func (me *ZapiPerf) addCounter(counter *node.Node, name, display string, enabled
 	return baseCounter
 }
 
-// override counter property
 func (me *ZapiPerf) GetOverride(counter string) string {
 	if o := me.Params.GetChildS("override"); o != nil {
 		return o.GetChildContentS(counter)
@@ -1070,8 +773,6 @@ func (me *ZapiPerf) GetOverride(counter string) string {
 	return ""
 }
 
-// parse ZAPI array counter (histogram), so we can store it
-// as multiple flat metrics
 func parseHistogramLabels(elem *node.Node) ([]string, string) {
 	var (
 		labels []string
@@ -1097,15 +798,14 @@ func parseHistogramLabels(elem *node.Node) ([]string, string) {
 	return labels, msg
 }
 
-// Update instance cache
 func (me *ZapiPerf) PollInstance() (*matrix.Matrix, error) {
 
 	var (
-		err                                        error
-		request, results                           *node.Node
-		oldInstances                               *set.Set
-		oldSize, newSize, removed, added           int
-		keyAttr, instancesAttr, nameAttr, uuidAttr string
+		err                              error
+		request, results                 *node.Node
+		oldInstances                     *set.Set
+		oldSize, newSize, removed, added int
+		instancesAttr                    string
 	)
 
 	oldInstances = set.New()
@@ -1116,41 +816,15 @@ func (me *ZapiPerf) PollInstance() (*matrix.Matrix, error) {
 
 	me.Logger.Debug().Msgf("updating instance cache (old cache has: %d)", oldInstances.Size())
 
-	nameAttr = "name"
-	uuidAttr = "uuid"
-	keyAttr = me.instanceKey
-
-	// hack for workload objects: get instances from Zapi
-	if me.Query == objWorkload || me.Query == objWorkloadDetail || me.Query == objWorkloadVolume || me.Query == objWorkloadDetailVolume {
-		request = node.NewXmlS("qos-workload-get-iter")
-		queryElem := request.NewChildS("query", "")
-		infoElem := queryElem.NewChildS("qos-workload-info", "")
-		if me.Query == objWorkloadVolume || me.Query == objWorkloadDetailVolume {
-			infoElem.NewChildS("workload-class", "autovolume")
-		} else {
-			infoElem.NewChildS("workload-class", "user-defined")
-		}
-
-		instancesAttr = "attributes-list"
-		nameAttr = "workload-name"
-		uuidAttr = "workload-uuid"
-		if me.instanceKey == "instance_name" || me.instanceKey == "name" {
-			keyAttr = "workload-name"
-		} else {
-			keyAttr = "workload-uuid"
-		}
-		// syntax for cdot/perf
-	} else if me.Client.IsClustered() {
+	if me.Client.IsClustered() {
 		request = node.NewXmlS("perf-object-instance-list-info-iter")
-		request.NewChildS("objectname", me.Query)
 		instancesAttr = "attributes-list"
-		// syntax for 7mode/perf
 	} else {
 		request = node.NewXmlS("perf-object-instance-list-info")
-		request.NewChildS("objectname", me.Query)
 		instancesAttr = "instances"
 	}
 
+	request.NewChildS("objectname", me.Query)
 	if me.Client.IsClustered() {
 		request.NewChildS("max-records", strconv.Itoa(me.batchSize))
 	}
@@ -1176,27 +850,18 @@ func (me *ZapiPerf) PollInstance() (*matrix.Matrix, error) {
 
 		for _, i := range instances.GetChildren() {
 
-			if key := i.GetChildContentS(keyAttr); key == "" {
+			if key := i.GetChildContentS(me.instanceKey); key == "" {
 				// instance key missing
-				name := i.GetChildContentS(nameAttr)
-				uuid := i.GetChildContentS(uuidAttr)
-				me.Logger.Debug().Msgf("skip instance, missing key [%s] (name=%s, uuid=%s)", me.instanceKey, name, uuid)
+				n := i.GetChildContentS("name")
+				u := i.GetChildContentS("uuid")
+				me.Logger.Debug().Msgf("skip instance, missing key [%s] (name=%s, uuid=%s)", me.instanceKey, n, u)
 			} else if oldInstances.Delete(key) {
 				// instance already in cache
-				me.Logger.Debug().Msgf("updated instance [%s%s%s%s]", color.Bold, color.Yellow, key, color.End)
 				continue
-			} else if instance, err := me.Matrix.NewInstance(key); err != nil {
-				me.Logger.Error().Err(err).Msg("add instance")
+			} else if _, err = me.Matrix.NewInstance(key); err != nil {
+				me.Logger.Warn().Msgf("add instance: %v", err)
 			} else {
 				me.Logger.Debug().Msgf("added new instance [%s]", key)
-				if me.Query == objWorkload || me.Query == objWorkloadDetail || me.Query == objWorkloadVolume || me.Query == objWorkloadDetailVolume {
-					for label, display := range me.qosLabels {
-						if value := i.GetChildContentS(label); value != "" {
-							instance.SetLabel(display, value)
-						}
-					}
-					me.Logger.Debug().Msgf("(%s) [%s] added QOS labels: %s", me.Query, key, instance.GetLabels().String())
-				}
 			}
 		}
 	}
@@ -1219,5 +884,7 @@ func (me *ZapiPerf) PollInstance() (*matrix.Matrix, error) {
 	return nil, err
 }
 
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
+// Interface guards
+var (
+	_ collector.Collector = (*ZapiPerf)(nil)
+)
diff --git a/cmd/exporters/influxdb/influxdb.go b/cmd/exporters/influxdb/influxdb.go
index eb294881..57bbd0f1 100644
--- a/cmd/exporters/influxdb/influxdb.go
+++ b/cmd/exporters/influxdb/influxdb.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package influxdb
 
 import (
 	"bytes"
@@ -337,6 +337,3 @@ func (e *InfluxDB) Render(data *matrix.Matrix) ([][]byte, error) {
 	}
 	return rendered, nil
 }
-
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
diff --git a/cmd/exporters/influxdb/measurement.go b/cmd/exporters/influxdb/measurement.go
index f0878074..08ef5bd2 100644
--- a/cmd/exporters/influxdb/measurement.go
+++ b/cmd/exporters/influxdb/measurement.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package influxdb
 
 import (
 	"fmt"
diff --git a/cmd/exporters/prometheus/cache.go b/cmd/exporters/prometheus/cache.go
index f491da5e..ae73864c 100644
--- a/cmd/exporters/prometheus/cache.go
+++ b/cmd/exporters/prometheus/cache.go
@@ -2,7 +2,7 @@
  * Copyright NetApp Inc, 2021 All rights reserved
  */
 
-package main
+package prometheus
 
 import (
 	"sync"
diff --git a/cmd/exporters/prometheus/html.go b/cmd/exporters/prometheus/html.go
index 4261ba11..b5f1fe9f 100644
--- a/cmd/exporters/prometheus/html.go
+++ b/cmd/exporters/prometheus/html.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package prometheus
 
 var html_template = `
 <!DOCTYPE html>
diff --git a/cmd/exporters/prometheus/httpd.go b/cmd/exporters/prometheus/httpd.go
index 849a31c1..c5ccd9d7 100644
--- a/cmd/exporters/prometheus/httpd.go
+++ b/cmd/exporters/prometheus/httpd.go
@@ -4,7 +4,7 @@
 
 // Package prometheus creates an HTTP end-point for Prometheus to scrape on `/metrics`
 //It also publishes a list of available metrics for human consumption on `/`
-package main
+package prometheus
 
 import (
 	"bytes"
@@ -25,7 +25,7 @@ func (me *Prometheus) startHttpD(addr, port string) {
 	server := &http.Server{Addr: addr + ":" + port, Handler: mux}
 
 	if err := server.ListenAndServe(); err != nil {
-		me.Logger.Fatal().Msgf("(httpd) %v", err.Error())
+		me.Logger.Fatal().Msgf(" (httpd) %v", err.Error())
 	} else {
 		me.Logger.Info().Msgf("(httpd) listening at [http://%s:%s]", addr, port)
 	}
diff --git a/cmd/exporters/prometheus/prometheus.go b/cmd/exporters/prometheus/prometheus.go
index 20e2eb88..34bf9f8e 100644
--- a/cmd/exporters/prometheus/prometheus.go
+++ b/cmd/exporters/prometheus/prometheus.go
@@ -19,7 +19,7 @@ Package Description:
    on the cache creates a race-condition (not caught on all Linux systems).
 */
 
-package main
+package prometheus
 
 import (
 	"fmt"
@@ -396,6 +396,3 @@ func (me *Prometheus) render(data *matrix.Matrix) ([][]byte, error) {
 	me.Logger.Debug().Msgf("rendered %d data points from %d (%s) instances", len(rendered), len(data.GetInstances()), data.Object)
 	return rendered, nil
 }
-
-// Need to appease go build - see https://github.com/golang/go/issues/20312
-func main() {}
diff --git a/cmd/poller/collector/collector.go b/cmd/poller/collector/collector.go
index 7f3762c9..284aebad 100644
--- a/cmd/poller/collector/collector.go
+++ b/cmd/poller/collector/collector.go
@@ -18,7 +18,6 @@ package collector
 import (
 	"goharvest2/pkg/conf"
 	"goharvest2/pkg/logging"
-	"path"
 	"reflect"
 	"runtime/debug"
 	"strconv"
@@ -26,7 +25,6 @@ import (
 	"sync"
 	"time"
 
-	"goharvest2/pkg/dload"
 	"goharvest2/pkg/errors"
 	"goharvest2/pkg/matrix"
 	"goharvest2/pkg/tree/node"
@@ -45,7 +43,7 @@ import (
 // Note that many of the functions required by the interface
 // are only there to facilitate "inheritance" through AbstractCollector.
 type Collector interface {
-	Init() error
+	Init(*AbstractCollector) error
 	Start(*sync.WaitGroup)
 	GetName() string
 	GetObject() string
@@ -60,11 +58,12 @@ type Collector interface {
 	SetMetadata(*matrix.Matrix)
 	WantedExporters(configFp string) []string
 	LinkExporter(exporter.Exporter)
-	LoadPlugins(*node.Node) error
+	LoadPlugins(*node.Node, Collector) error
+	LoadPlugin(string, *plugin.AbstractPlugin) plugin.Plugin
 }
 
-// CollectorStatus defines the possible states of a collector
-var CollectorStatus = [3]string{
+// Status defines the possible states of a collector
+var Status = [3]string{
 	"up",
 	"standby",
 	"failed",
@@ -127,9 +126,9 @@ func New(name, object string, options *options.Options, params *node.Node) *Abst
 //   data: 10s
 //   instance: 20s
 //
-// then we expect that the collector has methods PollDdta and PollInstance
+// then we expect that the collector has methods PollData and PollInstance
 // that need to be invoked every 10 and 20 seconds respectively.
-// Names of the polls are arbitrary, only "ddta" is a special case, since
+// Names of the polls are arbitrary, only "data" is a special case, since
 // plugins are executed after the data poll (this might change).
 func Init(c Collector) error {
 
@@ -147,7 +146,7 @@ func Init(c Collector) error {
 	s := schedule.New()
 
 	// Each task will be mapped to a collector method
-	// Example: "data" will be alligned to method PollData()
+	// Example: "data" will be aligned to method PollData()
 	for _, task := range tasks.GetChildren() {
 
 		methodName := "Poll" + strings.Title(task.GetNameS())
@@ -192,7 +191,7 @@ func Init(c Collector) error {
 
 	// Initialize Plugins
 	if plugins := params.GetChildS("plugins"); plugins != nil {
-		if err := c.LoadPlugins(plugins); err != nil {
+		if err := c.LoadPlugins(plugins, c); err != nil {
 			return err
 		}
 	}
@@ -218,7 +217,7 @@ func Init(c Collector) error {
 	//md.AddLabel("task", "")
 	//md.AddLabel("interval", "")
 
-	// add tasks of the collecor as metadata instances
+	// add tasks of the collector as metadata instances
 	for _, task := range s.GetTasks() {
 		instance, _ := md.NewInstance(task.Name)
 		instance.SetLabel("task", task.Name)
@@ -419,13 +418,13 @@ func (me *AbstractCollector) AddCollectCount(n uint64) {
 
 // GetStatus returns current state of the collector
 func (me *AbstractCollector) GetStatus() (uint8, string, string) {
-	return me.Status, CollectorStatus[me.Status], me.Message
+	return me.Status, Status[me.Status], me.Message
 }
 
 // SetStatus sets the current state of the collector to one
 // of the values defined by CollectorStatus
 func (me *AbstractCollector) SetStatus(status uint8, msg string) {
-	if status < 0 || status >= uint8(len(CollectorStatus)) {
+	if status < 0 || status >= uint8(len(Status)) {
 		panic("invalid status code " + strconv.Itoa(int(status)))
 	}
 	me.Status = status
@@ -473,9 +472,13 @@ func (me *AbstractCollector) LinkExporter(e exporter.Exporter) {
 	me.Exporters = append(me.Exporters, e)
 }
 
-// LoadPlugins loads built-in plugins or dynamically loads custom plugins
-// and adds them to the collector
-func (me *AbstractCollector) LoadPlugins(params *node.Node) error {
+func (me *AbstractCollector) LoadPlugin(s string, abc *plugin.AbstractPlugin) plugin.Plugin {
+	return nil
+}
+
+//LoadPlugins loads built-in plugins or dynamically loads custom plugins
+//and adds them to the collector
+func (me *AbstractCollector) LoadPlugins(params *node.Node, c Collector) error {
 
 	var p plugin.Plugin
 	var abc *plugin.AbstractPlugin
@@ -495,20 +498,8 @@ func (me *AbstractCollector) LoadPlugins(params *node.Node) error {
 			me.Logger.Debug().Msgf("loaded built-in plugin [%s]", name)
 			// case 2: available as dynamic plugin
 		} else {
-			binpath := path.Join(me.Options.HomePath, "bin", "plugins", strings.ToLower(me.Name))
-			module, err := dload.LoadFuncFromModule(binpath, strings.ToLower(name), "New")
-			if err != nil {
-				//logger.Error(c.LongName, "load plugin [%s]: %v", name, err)
-				return errors.New(errors.ERR_DLOAD, "plugin "+name+": "+err.Error())
-			}
-
-			NewFunc, ok := module.(func(*plugin.AbstractPlugin) plugin.Plugin)
-			if !ok {
-				//logger.Error(c.LongName, "load plugin [%s]: New() has not expected signature", name)
-				return errors.New(errors.ERR_DLOAD, name+": New()")
-			}
-			p = NewFunc(abc)
-			me.Logger.Debug().Msgf("loaded dynamic plugin [%s]", name)
+			p = c.LoadPlugin(name, abc)
+			me.Logger.Debug().Msgf("loaded plugin [%s]", name)
 		}
 
 		if err := p.Init(); err != nil {
diff --git a/cmd/poller/collector/helpers.go b/cmd/poller/collector/helpers.go
index 7f1ac929..6fd6117b 100644
--- a/cmd/poller/collector/helpers.go
+++ b/cmd/poller/collector/helpers.go
@@ -38,7 +38,7 @@ func ImportTemplate(confPath, confFn, collectorName string) (*node.Node, error)
 // ImportSubTemplate retrieves the best matching subtemplate of a collector object.
 //
 // This method is only applicable to the Zapi/ZapiPerf collectors which have
-// multiple objects and each object is forked as a seperate collector.
+// multiple objects and each object is forked as a separate collector.
 // The subtemplates are sorted in subdirectories that serve as "tag" for the
 // matching ONTAP version. ImportSubTemplate will attempt to choose the subtemplate
 // with closest matching ONTAP version.
@@ -60,7 +60,7 @@ func (c *AbstractCollector) ImportSubTemplate(model, filename string, version [3
 	pathPrefix = path.Join(c.Options.HomePath, "conf/", strings.ToLower(c.Name), model)
 	c.Logger.Debug().Msgf("Looking for best-fitting template in [%s]", pathPrefix)
 
-	// check for available versons, those are the subdirectories that include filename
+	// check for available versions, those are the subdirectories that include filename
 	availableVersions = make(map[string]bool)
 	if files, err := ioutil.ReadDir(pathPrefix); err == nil {
 		for _, file := range files {
diff --git a/cmd/poller/plugin/plugin.go b/cmd/poller/plugin/plugin.go
index 634df1de..7f9c7704 100644
--- a/cmd/poller/plugin/plugin.go
+++ b/cmd/poller/plugin/plugin.go
@@ -1,13 +1,13 @@
-/*
- * Copyright NetApp Inc, 2021 All rights reserved
+//Copyright NetApp Inc, 2021 All rights reserved
 
+/*
 	Package plugin provides abstractions for plugins, as well as
 	a number of generic built-in plugins. Plugins allow to customize
 	and manipulate data from collectors and sometimes collect additional
 	data without changing the sourcecode of collectors. Multiple plugins
 	can be put in a pipeline, they are executed in the same order as they
 	are defined in the collector's config file.
-	Harvest architecuture defines three types of plugins:
+	Harvest architecture defines three types of plugins:
 
 	**built-in**
     	Statically compiled, generic plugins. "Generic" means
@@ -28,11 +28,13 @@
 package plugin
 
 import (
+	"fmt"
 	"goharvest2/cmd/poller/options"
 	"goharvest2/pkg/errors"
 	"goharvest2/pkg/logging"
 	"goharvest2/pkg/matrix"
 	"goharvest2/pkg/tree/node"
+	"sync"
 )
 
 // Plugin defines the methods of a plugin
@@ -42,6 +44,57 @@ type Plugin interface {
 	Run(*matrix.Matrix) ([]*matrix.Matrix, error)
 }
 
+var (
+	modules   = make(map[string]ModuleInfo)
+	modulesMu sync.RWMutex
+)
+
+// GetModule returns module information from its ID (full name).
+func GetModule(name string) (ModuleInfo, error) {
+	modulesMu.RLock()
+	defer modulesMu.RUnlock()
+	m, ok := modules[name]
+	if !ok {
+		return ModuleInfo{}, fmt.Errorf("module not registered: %s", name)
+	}
+	return m, nil
+}
+
+func RegisterModule(instance Module) {
+	mod := instance.HarvestModule()
+
+	if mod.ID == "" {
+		panic("module missing ID")
+	}
+	if mod.ID == "harvest" || mod.ID == "admin" {
+		panic(fmt.Sprintf("module ID '%s' is reserved", mod.ID))
+	}
+	if mod.New == nil {
+		panic("missing ModuleInfo.New")
+	}
+	if val := mod.New(); val == nil {
+		panic("ModuleInfo.New must return a non-nil module instance")
+	}
+	modulesMu.Lock()
+	defer modulesMu.Unlock()
+
+	if _, ok := modules[mod.ID]; ok {
+		panic(fmt.Sprintf("module already registered: %s", mod.ID))
+	}
+	modules[mod.ID] = mod
+}
+
+type Module interface {
+	HarvestModule() ModuleInfo
+}
+
+type ModuleInfo struct {
+	// name of module
+	ID string
+
+	New func() Module
+}
+
 // AbstractPlugin implements methods of the Plugin interface, except Run()
 type AbstractPlugin struct {
 	Parent       string
diff --git a/cmd/poller/poller.go b/cmd/poller/poller.go
index 97c8cdae..14698133 100644
--- a/cmd/poller/poller.go
+++ b/cmd/poller/poller.go
@@ -26,13 +26,18 @@ package main
 import (
 	"fmt"
 	"github.com/spf13/cobra"
+	_ "goharvest2/cmd/collectors/unix"
+	_ "goharvest2/cmd/collectors/zapi/collector"
+	_ "goharvest2/cmd/collectors/zapiperf"
+	"goharvest2/cmd/exporters/influxdb"
+	"goharvest2/cmd/exporters/prometheus"
 	"goharvest2/cmd/harvest/version"
 	"goharvest2/cmd/poller/collector"
 	"goharvest2/cmd/poller/exporter"
 	"goharvest2/cmd/poller/options"
+	"goharvest2/cmd/poller/plugin"
 	"goharvest2/cmd/poller/schedule"
 	"goharvest2/pkg/conf"
-	"goharvest2/pkg/dload"
 	"goharvest2/pkg/errors"
 	"goharvest2/pkg/logging"
 	"goharvest2/pkg/matrix"
@@ -43,9 +48,7 @@ import (
 	"os/exec"
 	"os/signal"
 	"path"
-	"plugin"
 	"runtime"
-	"runtime/debug"
 	"strconv"
 	"strings"
 	"sync"
@@ -54,18 +57,17 @@ import (
 
 // default params
 var (
-	pollerSchedule  string = "60s"
-	logFileName     string = ""
-	logMaxMegaBytes int    = logging.DefaultLogMaxMegaBytes // 10MB
-	logMaxBackups   int    = logging.DefaultLogMaxBackups
-	logMaxAge       int    = logging.DefaultLogMaxAge
+	pollerSchedule  = "60s"
+	logFileName     = ""
+	logMaxMegaBytes = logging.DefaultLogMaxMegaBytes // 10MB
+	logMaxBackups   = logging.DefaultLogMaxBackups
+	logMaxAge       = logging.DefaultLogMaxAge
 )
 
-// init with default configuration
-// by default it gets logged only to console
-var logger *logging.Logger = logging.Get()
+// init with default configuration by default it gets logged both to console and  harvest.log
+var logger = logging.Get()
 
-// signals to catch
+// SIGNALS to catch
 var SIGNALS = []os.Signal{
 	syscall.SIGHUP,
 	syscall.SIGINT,
@@ -81,23 +83,18 @@ var deprecatedCollectors = map[string]string{
 // Poller is the instance that starts and monitors a
 // group of collectors and exporters as a single UNIX process
 type Poller struct {
-	name            string
-	target          string
-	options         *options.Options
-	pid             int
-	pidf            string
-	schedule        *schedule.Schedule
-	collectors      []collector.Collector
-	exporters       []exporter.Exporter
-	exporter_params *node.Node
-	params          *node.Node
-	metadata        *matrix.Matrix
-	status          *matrix.Matrix
-}
-
-// New returns a new instance of Poller
-func New() *Poller {
-	return &Poller{}
+	name           string
+	target         string
+	options        *options.Options
+	pid            int
+	pidf           string
+	schedule       *schedule.Schedule
+	collectors     []collector.Collector
+	exporters      []exporter.Exporter
+	exporterParams *node.Node
+	params         *node.Node
+	metadata       *matrix.Matrix
+	status         *matrix.Matrix
 }
 
 // Init starts Poller, reads parameters, opens zeroLog handler, initializes metadata,
@@ -123,8 +120,8 @@ func (me *Poller) Init() error {
 	}
 
 	if me.params, err = conf.GetPoller(me.options.Config, me.name); err != nil {
-		// seperate logger is not yet configured as it depends on setting logMaxMegaBytes, logMaxFiles later
-		// Using default insance of logger which logs below error to harvest.log
+		// separate logger is not yet configured as it depends on setting logMaxMegaBytes, logMaxFiles later
+		// Using default instance of logger which logs below error to harvest.log
 		logging.SubLogger("Poller", me.name).Error().Stack().Err(err).Msg("read config")
 		return err
 	}
@@ -223,9 +220,9 @@ func (me *Poller) Init() error {
 
 	// initialize our metadata, the metadata will host status of our
 	// collectors and exporters, as well as ping stats to target host
-	me.load_metadata()
+	me.loadMetadata()
 
-	if me.exporter_params, err = conf.GetExporters(me.options.Config); err != nil {
+	if me.exporterParams, err = conf.GetExporters(me.options.Config); err != nil {
 		logger.Warn().Msgf("read exporter params: %v", err)
 		// @TODO just warn or abort?
 	}
@@ -254,7 +251,7 @@ func (me *Poller) Init() error {
 				continue
 			}
 
-			if err = me.load_collector(c, ""); err != nil {
+			if err = me.loadCollector(c, ""); err != nil {
 				logger.Error().Stack().Err(err).Msgf("load collector (%s):", c)
 			}
 		}
@@ -459,9 +456,9 @@ func (me *Poller) registerPid() error {
 }
 
 // set up signal disposition
-func (me *Poller) handleSignals(signal_channel chan os.Signal) {
+func (me *Poller) handleSignals(signalChannel chan os.Signal) {
 	for {
-		sig := <-signal_channel
+		sig := <-signalChannel
 		logger.Info().Msgf("caught signal [%s]", sig)
 		me.Stop()
 		os.Exit(0)
@@ -489,20 +486,15 @@ func (me *Poller) ping() (float32, bool) {
 // dynamically load and initialize a collector
 // if there are more than one objects defined for a collector,
 // then multiple collectors will be initialized
-func (me *Poller) load_collector(class, object string) error {
+func (me *Poller) loadCollector(class, object string) error {
 
 	var (
 		err              error
-		sym              plugin.Symbol
-		binpath          string
 		template, custom *node.Node
 		collectors       []collector.Collector
 		col              collector.Collector
 	)
 
-	// path to the shared object (.so file)
-	binpath = path.Join(me.options.HomePath, "bin", "collectors")
-
 	// throw warning for deprecated collectors
 	if r, d := deprecatedCollectors[strings.ToLower(class)]; d {
 		if r != "" {
@@ -512,15 +504,6 @@ func (me *Poller) load_collector(class, object string) error {
 		}
 	}
 
-	if sym, err = dload.LoadFuncFromModule(binpath, strings.ToLower(class), "New"); err != nil {
-		return err
-	}
-
-	NewFunc, ok := sym.(func(*collector.AbstractCollector) collector.Collector)
-	if !ok {
-		return errors.New(errors.ERR_DLOAD, "New() has not expected signature")
-	}
-
 	// load the template file(s) of the collector where we expect to find
 	// object name or list of objects
 	if template, err = collector.ImportTemplate(me.options.HomePath, "default.yaml", class); err != nil {
@@ -541,14 +524,16 @@ func (me *Poller) load_collector(class, object string) error {
 		object = template.GetChildContentS("object")
 	}
 
-	// if object is defined, we only initialize 1 subcollector / object
+	// if object is defined, we only initialize 1 sub-collector / object
 	if object != "" {
-		col = NewFunc(collector.New(class, object, me.options, template.Copy()))
-		if err = col.Init(); err != nil {
-			logger.Error().Msgf("init collector (%s:%s): %v", class, object, err)
-		} else {
-			collectors = append(collectors, col)
-			logger.Debug().Msgf("initialized collector (%s:%s)", class, object)
+		col, err = me.newCollector(class, object, template)
+		if col != nil {
+			if err != nil {
+				logger.Error().Msgf("init collector (%s:%s): %v", class, object, err)
+			} else {
+				collectors = append(collectors, col)
+				logger.Debug().Msgf("initialized collector (%s:%s)", class, object)
+			}
 		}
 		// if template has list of objects, initialize 1 subcollector for each
 	} else if objects := template.GetChildS("objects"); objects != nil {
@@ -572,8 +557,12 @@ func (me *Poller) load_collector(class, object string) error {
 				continue
 			}
 
-			col = NewFunc(collector.New(class, object.GetNameS(), me.options, template.Copy()))
-			if err = col.Init(); err != nil {
+			col, err = me.newCollector(class, object.GetNameS(), template)
+			if col == nil {
+				logger.Warn().Msgf("collector is nil for collector-object (%s:%s)", class, object.GetNameS())
+				continue
+			}
+			if err != nil {
 				logger.Warn().Msgf("init collector-object (%s:%s): %v", class, object.GetNameS(), err)
 				if errors.IsErr(err, errors.ERR_CONNECTION) {
 					logger.Warn().Msgf("aborting collector (%s)", class)
@@ -592,17 +581,20 @@ func (me *Poller) load_collector(class, object string) error {
 	logger.Debug().Msgf("initialized (%s) with %d objects", class, len(collectors))
 	// link each collector with requested exporter & update metadata
 	for _, col = range collectors {
-
+		if col == nil {
+			logger.Warn().Msg("ignoring nil collector")
+			continue
+		}
 		name := col.GetName()
 		obj := col.GetObject()
 
-		for _, exp_name := range col.WantedExporters(me.options.Config) {
-			logger.Trace().Msgf("exp_name %s", exp_name)
-			if exp := me.load_exporter(exp_name); exp != nil {
+		for _, expName := range col.WantedExporters(me.options.Config) {
+			logger.Trace().Msgf("expName %s", expName)
+			if exp := me.loadExporter(expName); exp != nil {
 				col.LinkExporter(exp)
-				logger.Debug().Msgf("linked (%s:%s) to exporter (%s)", name, obj, exp_name)
+				logger.Debug().Msgf("linked (%s:%s) to exporter (%s)", name, obj, expName)
 			} else {
-				logger.Warn().Msgf("exporter (%s) requested by (%s:%s) not available", exp_name, name, obj)
+				logger.Warn().Msgf("exporter (%s) requested by (%s:%s) not available", expName, name, obj)
 			}
 		}
 
@@ -620,24 +612,41 @@ func (me *Poller) load_collector(class, object string) error {
 	return nil
 }
 
+func (me *Poller) newCollector(class string, object string, template *node.Node) (collector.Collector, error) {
+	name := "harvest.collector." + strings.ToLower(class)
+	mod, err := plugin.GetModule(name)
+	if err != nil {
+		logger.Error().Msgf("error getting module %s", name)
+		return nil, err
+	}
+	inst := mod.New()
+	col, ok := inst.(collector.Collector)
+	if !ok {
+		logger.Error().Msgf("collector '%s' is not a Collector", name)
+		return nil, errors.New(errors.ERR_NO_COLLECTOR, "no collectors")
+	}
+	delegate := collector.New(class, object, me.options, template.Copy())
+	err = col.Init(delegate)
+	return col, err
+}
+
 // returns exporter that matches to name, if exporter is not loaded
 // tries to load and return
-func (me *Poller) load_exporter(name string) exporter.Exporter {
+func (me *Poller) loadExporter(name string) exporter.Exporter {
 
 	var (
-		err            error
-		sym            plugin.Symbol
-		binpath, class string
-		params         *node.Node
-		exp            exporter.Exporter
+		err    error
+		class  string
+		params *node.Node
+		exp    exporter.Exporter
 	)
 
 	// stop here if exporter is already loaded
-	if exp = me.get_exporter(name); exp != nil {
+	if exp = me.getExporter(name); exp != nil {
 		return exp
 	}
 
-	if params = me.exporter_params.GetChildS(name); params == nil {
+	if params = me.exporterParams.GetChildS(name); params == nil {
 		logger.Warn().Msgf("exporter (%s) not defined in config", name)
 		return nil
 	}
@@ -647,20 +656,16 @@ func (me *Poller) load_exporter(name string) exporter.Exporter {
 		return nil
 	}
 
-	binpath = path.Join(me.options.HomePath, "bin", "exporters")
-
-	if sym, err = dload.LoadFuncFromModule(binpath, strings.ToLower(class), "New"); err != nil {
-		logger.Error().Msgf("dload: %v", err.Error())
+	absExp := exporter.New(class, name, me.options, params)
+	switch class {
+	case "Prometheus":
+		exp = prometheus.New(absExp)
+	case "InfluxDB":
+		exp = influxdb.New(absExp)
+	default:
+		logger.Error().Msgf("no exporter of name:type %s:%s", name, class)
 		return nil
 	}
-
-	NewFunc, ok := sym.(func(*exporter.AbstractExporter) exporter.Exporter)
-	if !ok {
-		logger.Error().Msg("New() has not expected signature")
-		return nil
-	}
-
-	exp = NewFunc(exporter.New(class, name, me.options, params))
 	if err = exp.Init(); err != nil {
 		logger.Error().Msgf("init exporter (%s): %v", name, err)
 		return nil
@@ -681,7 +686,7 @@ func (me *Poller) load_exporter(name string) exporter.Exporter {
 
 }
 
-func (me *Poller) get_exporter(name string) exporter.Exporter {
+func (me *Poller) getExporter(name string) exporter.Exporter {
 	for _, exp := range me.exporters {
 		if exp.GetName() == name {
 			return exp
@@ -691,7 +696,7 @@ func (me *Poller) get_exporter(name string) exporter.Exporter {
 }
 
 // initialize matrices to be used as metadata
-func (me *Poller) load_metadata() {
+func (me *Poller) loadMetadata() {
 
 	me.metadata = matrix.New("poller", "metadata_component")
 	me.metadata.NewMetricUint8("status")
@@ -722,9 +727,9 @@ var pollerCmd = &cobra.Command{
 	Run:   startPoller,
 }
 
-func startPoller(cmd *cobra.Command, _ []string) {
+func startPoller(_ *cobra.Command, _ []string) {
 	//cmd.DebugFlags()  // uncomment to print flags
-	poller := New()
+	poller := &Poller{}
 	poller.options = &args
 	if poller.Init() != nil {
 		// error already logger by poller
@@ -756,12 +761,21 @@ func init() {
 	_ = pollerCmd.MarkFlagRequired("poller")
 }
 
-// start poller, if fails write to log
+// start poller, if fails try to write to syslog
 func main() {
 
+	// don't recover if a goroutine has panicked, instead
+	// try to zeroLog as much as possible, since normally it's
+	// not properly logged
 	defer func() {
+		//logger.Warn("(main) ", "defer func here")
 		if r := recover(); r != nil {
-			logger.Fatal().Stack().Err(errors.New(errors.GO_ROUTINE_PANIC, string(debug.Stack()))).Msg(`(main) terminating abnormally, tip: run in foreground mode (with "--loglevel 0") to debug`)
+			logger.Info().Msgf("harvest poller panicked: %v", r)
+			// if logger still available try to write there as well
+			// do this last, since might make us panic as again
+			logger.Fatal().Msgf("(main) %v", r)
+			logger.Fatal().Msg(`(main) terminating abnormally, tip: run in foreground mode (with "--loglevel 0") to debug`)
+
 			os.Exit(1)
 		}
 	}()
diff --git a/pkg/api/ontapi/zapi/client.go b/pkg/api/ontapi/zapi/client.go
index bceaccad..b1abcccd 100644
--- a/pkg/api/ontapi/zapi/client.go
+++ b/pkg/api/ontapi/zapi/client.go
@@ -1,9 +1,7 @@
-/*
- * Copyright NetApp Inc, 2021 All rights reserved
+// Copyright NetApp Inc, 2021 All rights reserved
 
-Package zapi provides type Client for connecting to a C-dot or 7-mode
-Ontap cluster and sending API requests using the ZAPI protocol.
-*/
+// Package zapi provides type Client for connecting to a C-dot or 7-mode
+// ONTAP cluster and sending API requests using the ZAPI protocol.
 package zapi
 
 import (
@@ -66,8 +64,11 @@ func New(config *node.Node) (*Client, error) {
 		return nil, errors.New(errors.MISSING_PARAM, "addr")
 	}
 
-	url = "https://" + addr + ":443/servlets/netapp.servlets.admin.XMLrequest_filer"
-
+	if config.GetChildContentS("is_kfs") == "true" {
+		url = "https://" + addr + ":8443/servlets/netapp.servlets.admin.XMLrequest_filer"
+	} else {
+		url = "https://" + addr + ":443/servlets/netapp.servlets.admin.XMLrequest_filer"
+	}
 	// create a request object that will be used for later requests
 	if request, err = http.NewRequest("POST", url, nil); err != nil {
 		return nil, err
@@ -76,7 +77,7 @@ func New(config *node.Node) (*Client, error) {
 	request.Header.Set("Content-type", "text/xml")
 	request.Header.Set("Charset", "utf-8")
 
-	// by default, encorce secure TLS, if not requested otherwise by user
+	// by default, enforce secure TLS, if not requested otherwise by user
 	if x := config.GetChildContentS("use_insecure_tls"); x != "" {
 		if useInsecureTLS, err = strconv.ParseBool(x); err != nil {
 			client.Logger.Error().Stack().Err(err).Msg("use_insecure_tls")
@@ -143,7 +144,7 @@ func New(config *node.Node) (*Client, error) {
 	return &client, nil
 }
 
-// init connects to the cluster and retrieves system info
+// Init connects to the cluster and retrieves system info
 // it will give up after retries
 func (c *Client) Init(retries int) error {
 	var err error
@@ -165,7 +166,7 @@ func (c *Client) IsClustered() bool {
 	return c.system.clustered
 }
 
-// Version returns version of the ONTAP server (generation, manjor and minor)
+// Version returns version of the ONTAP server (generation, major and minor)
 func (c *Client) Version() [3]int {
 	return c.system.version
 }
@@ -199,7 +200,7 @@ func (c *Client) BuildRequestString(request string) error {
 }
 
 // BuildRequest builds an API request from the node query
-// root element of the request is usualy the API name (e.g. "volume-get-iter") and
+// root element of the request is usually the API name (e.g. "volume-get-iter") and
 // its children are the attributes requested
 func (c *Client) BuildRequest(request *node.Node) error {
 	return c.buildRequest(request, false)
@@ -239,14 +240,14 @@ func (c *Client) buildRequest(query *node.Node, forceCluster bool) error {
 }
 
 // Invoke will issue the API request and return server response
-// this method should only be callled after building the request
+// this method should only be called after building the request
 func (c *Client) Invoke() (*node.Node, error) {
 	result, _, _, err := c.invoke(false)
 	return result, err
 }
 
 // InvokeBatchRequest will issue API requests in series, once there
-// are no more instances returned by the server, returned results will be nill
+// are no more instances returned by the server, returned results will be nil
 // Use the returned tag for subsequent calls to this method
 func (c *Client) InvokeBatchRequest(request *node.Node, tag string) (*node.Node, string, error) {
 	// wasteful of course, need to rewrite later @TODO
@@ -254,7 +255,7 @@ func (c *Client) InvokeBatchRequest(request *node.Node, tag string) (*node.Node,
 	return results, tag, err
 }
 
-// InvokeBatchRequestWithTimers does the same as InvokeBatchRequest, but it also
+// InvokeBatchWithTimers does the same as InvokeBatchRequest, but it also
 // returns API time and XML parse time
 func (c *Client) InvokeBatchWithTimers(request *node.Node, tag string) (*node.Node, string, time.Duration, time.Duration, error) {
 
@@ -311,13 +312,13 @@ func (c *Client) InvokeRequest(request *node.Node) (*node.Node, error) {
 
 // InvokeWithTimers invokes the request and returns parsed XML response and timers:
 // API wait time and XML parse time.
-// This method should only be callled after building the request
+// This method should only be called after building the request
 func (c *Client) InvokeWithTimers() (*node.Node, time.Duration, time.Duration, error) {
 	return c.invoke(true)
 }
 
-// InvokeWithTimers invokes the request and returns the raw server response
-// This method should only be callled after building the request
+// InvokeRaw invokes the request and returns the raw server response
+// This method should only be called after building the request
 func (c *Client) InvokeRaw() ([]byte, error) {
 	var (
 		response *http.Response
diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go
index 631d812c..f562b500 100644
--- a/pkg/conf/conf.go
+++ b/pkg/conf/conf.go
@@ -260,6 +260,7 @@ type Poller struct {
 	LogMaxFiles    *int      `yaml:"log_max_files,omitempty"`
 	Exporters      *[]string `yaml:"exporters,omitempty"`
 	Collectors     *[]string `yaml:"collectors,omitempty"`
+	IsKfs          *bool     `yaml:"is_kfs,omitempty"`
 }
 
 type Exporter struct {
diff --git a/pkg/tree/node/node.go b/pkg/tree/node/node.go
index 935a8858..5c355641 100644
--- a/pkg/tree/node/node.go
+++ b/pkg/tree/node/node.go
@@ -35,7 +35,7 @@ func NewXml(name []byte) *Node {
 
 func NewXmlS(name string) *Node {
 	// ugly solution to support xml
-	return &Node{XMLName: xml.Name{"", name}}
+	return &Node{XMLName: xml.Name{Local: name}}
 }
 
 func (n *Node) GetXmlNameS() string {
@@ -43,7 +43,7 @@ func (n *Node) GetXmlNameS() string {
 }
 
 func (n *Node) SetXmlNameS(name string) {
-	n.XMLName = xml.Name{"", name}
+	n.XMLName = xml.Name{Local: name}
 }
 
 func (n *Node) GetName() []byte {
@@ -94,7 +94,7 @@ func (n *Node) AddAttr(attr xml.Attr) {
 }
 
 func (n *Node) NewAttrS(name, value string) {
-	n.AddAttr(xml.Attr{Name: xml.Name{"", name}, Value: value})
+	n.AddAttr(xml.Attr{Name: xml.Name{Local: name}, Value: value})
 }
 
 func (n *Node) GetChildren() []*Node {


Test patch:
diff --git a/cmd/exporters/influxdb/influxdb_test.go b/cmd/exporters/influxdb/influxdb_test.go
index 3cdaf9c1..5b96dfba 100644
--- a/cmd/exporters/influxdb/influxdb_test.go
+++ b/cmd/exporters/influxdb/influxdb_test.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package influxdb
 
 import (
 	"goharvest2/cmd/poller/exporter"
diff --git a/cmd/exporters/influxdb/measurement_test.go b/cmd/exporters/influxdb/measurement_test.go
index 00b4ab47..e9c803a2 100644
--- a/cmd/exporters/influxdb/measurement_test.go
+++ b/cmd/exporters/influxdb/measurement_test.go
@@ -1,7 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
-package main
+package influxdb
 
 import "testing"
 
diff --git a/cmd/tools/doctor/doctor_test.go b/cmd/tools/doctor/doctor_test.go
index b0131ae4..e5f87fe7 100644
--- a/cmd/tools/doctor/doctor_test.go
+++ b/cmd/tools/doctor/doctor_test.go
@@ -27,7 +27,7 @@ func assertRedacted(t *testing.T, input, redacted string) {
 }
 
 func TestConfigToStruct(t *testing.T) {
-	path := "testConfig.yml"
+	path := "testdata/testConfig.yml"
 	err := conf.LoadHarvestConfig(path)
 	if err != nil {
 		return
@@ -62,9 +62,17 @@ func TestConfigToStruct(t *testing.T) {
 		t.Fatalf(`expected addr to be "www.example.com/influxdb", actual=%+v`, (*influxyURL))
 	}
 
-	collectors := (*conf.Config.Pollers)["infinity2"].Collectors
+	infinity := (*conf.Config.Pollers)["infinity2"]
+	collectors := infinity.Collectors
 	if (*collectors)[0] != "Zapi" {
 		t.Fatalf(`expected infinity2 collectors to contain Zapi actual=%+v`,
 			(*collectors)[0])
 	}
+	if infinity.IsKfs != nil && !*infinity.IsKfs {
+		t.Fatalf(`expected infinity2 is_kfs to be false, but was true`)
+	}
+	sim1 := (*conf.Config.Pollers)["sim-0001"]
+	if !*sim1.IsKfs {
+		t.Fatalf(`expected sim-0001 is_kfs to be true, but was false`)
+	}
 }
diff --git a/cmd/tools/doctor/testConfig.yml b/cmd/tools/doctor/testdata/testConfig.yml
similarity index 98%
rename from cmd/tools/doctor/testConfig.yml
rename to cmd/tools/doctor/testdata/testConfig.yml
index 73d67e39..a1e69619 100644
--- a/cmd/tools/doctor/testConfig.yml
+++ b/cmd/tools/doctor/testdata/testConfig.yml
@@ -83,4 +83,7 @@ Pollers:
     addr: 10.0.0.2
     collectors: [ Zapi ]
 
+  sim-0001:
+    is_kfs: true
+
 ll: grafana_api_token grafana_api_token