diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d72e8125..bae5eaf1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -46,7 +46,7 @@ jobs:
           password: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Release with goreleaser
-        uses: goreleaser/goreleaser-action@v4
+        uses: goreleaser/goreleaser-action@v5
         with:
           version: ${{ env.GORELEASER_VER }}
           args: release --rm-dist
diff --git a/app/api.go b/app/api.go
index d85e8c25..df6b0079 100644
--- a/app/api.go
+++ b/app/api.go
@@ -288,7 +288,7 @@ func (a *App) handleClusteringGet(w http.ResponseWriter, r *http.Request) {
 }
 
 func (a *App) handleHealthzGet(w http.ResponseWriter, r *http.Request) {
-	s := map[string]string{"status": "healthy",}
+	s := map[string]string{"status": "healthy"}
 	b, err := json.Marshal(s)
 	if err != nil {
 		w.WriteHeader(http.StatusInternalServerError)
diff --git a/app/clustering.go b/app/clustering.go
index c580568c..a08b756c 100644
--- a/app/clustering.go
+++ b/app/clustering.go
@@ -670,7 +670,7 @@ func (a *App) unassignTarget(ctx context.Context, name string, serviceID string)
 		}
 		rsp, err := client.Do(req)
 		if err != nil {
-			rsp.Body.Close()
+			// don't close the body here since Body will be nil
 			a.Logger.Printf("failed HTTP request: %v", err)
 			continue
 		}
diff --git a/app/diff.go b/app/diff.go
index 8334a9f0..5d331891 100644
--- a/app/diff.go
+++ b/app/diff.go
@@ -19,13 +19,14 @@ import (
 	"strings"
 
 	"github.com/openconfig/gnmi/proto/gnmi"
-	"github.com/openconfig/gnmic/config"
-	"github.com/openconfig/gnmic/formatters"
-	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/grpctunnel/tunnel"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"google.golang.org/protobuf/proto"
+
+	"github.com/openconfig/gnmic/config"
+	"github.com/openconfig/gnmic/formatters"
+	"github.com/openconfig/gnmic/types"
 )
 
 type targetDiffResponse struct {
diff --git a/app/gnmi_client_subscribe.go b/app/gnmi_client_subscribe.go
index fb1ac3a9..8a5dd7da 100644
--- a/app/gnmi_client_subscribe.go
+++ b/app/gnmi_client_subscribe.go
@@ -18,12 +18,13 @@ import (
 	"time"
 
 	"github.com/openconfig/gnmi/proto/gnmi"
+	"github.com/openconfig/grpctunnel/tunnel"
+	"google.golang.org/grpc"
+
 	"github.com/openconfig/gnmic/config"
 	"github.com/openconfig/gnmic/lockers"
 	"github.com/openconfig/gnmic/outputs"
 	"github.com/openconfig/gnmic/types"
-	"github.com/openconfig/grpctunnel/tunnel"
-	"google.golang.org/grpc"
 )
 
 type subscriptionRequest struct {
@@ -174,7 +175,7 @@ func (a *App) clientSubscribe(ctx context.Context, tc *types.TargetConfig) error
 	}
 	subRequests := make([]subscriptionRequest, 0, len(subscriptionsConfigs))
 	for scName, sc := range subscriptionsConfigs {
-		req, err := a.Config.CreateSubscribeRequest(sc, tc.Name)
+		req, err := a.Config.CreateSubscribeRequest(sc, tc)
 		if err != nil {
 			if errors.Is(errors.Unwrap(err), config.ErrConfig) {
 				fmt.Fprintf(os.Stderr, "%v\n", err)
@@ -243,7 +244,7 @@ func (a *App) clientSubscribeOnce(ctx context.Context, tc *types.TargetConfig) e
 	}
 	subRequests := make([]subscriptionRequest, 0)
 	for _, sc := range subscriptionsConfigs {
-		req, err := a.Config.CreateSubscribeRequest(sc, tc.Name)
+		req, err := a.Config.CreateSubscribeRequest(sc, tc)
 		if err != nil {
 			if errors.Is(errors.Unwrap(err), config.ErrConfig) {
 				fmt.Fprintf(os.Stderr, "%v\n", err)
diff --git a/app/metrics.go b/app/metrics.go
index ec3c042f..7c6a7aa6 100644
--- a/app/metrics.go
+++ b/app/metrics.go
@@ -69,7 +69,7 @@ func (a *App) startClusterMetrics() {
 			if err != nil {
 				a.Logger.Printf("failed to get leader key: %v", err)
 			}
-			if leader[leaderKey] == a.Config.InstanceName {
+			if leader[leaderKey] == a.Config.Clustering.InstanceName {
 				clusterIsLeader.Set(1)
 			} else {
 				clusterIsLeader.Set(0)
@@ -84,7 +84,7 @@ func (a *App) startClusterMetrics() {
 			}
 			numLockedNodes := 0
 			for _, v := range lockedNodes {
-				if v == a.Config.InstanceName {
+				if v == a.Config.Clustering.InstanceName {
 					numLockedNodes++
 				}
 			}
diff --git a/cache/oc_cache.go b/cache/oc_cache.go
index 5e16a0aa..1d044326 100644
--- a/cache/oc_cache.go
+++ b/cache/oc_cache.go
@@ -23,8 +23,9 @@ import (
 	"github.com/openconfig/gnmi/path"
 	"github.com/openconfig/gnmi/proto/gnmi"
 	"github.com/openconfig/gnmi/subscribe"
-	"github.com/openconfig/gnmic/utils"
 	"google.golang.org/protobuf/proto"
+
+	"github.com/openconfig/gnmic/utils"
 )
 
 const (
@@ -298,13 +299,13 @@ func (gc *gnmiCache) handleOnChangeQuery(ctx context.Context, ro *ReadOpts, ch c
 	caches := gc.getCaches(ro.Subscription)
 	numCaches := len(caches)
 	gc.logger.Printf("on-change query got %d caches", numCaches)
+
 	wg := new(sync.WaitGroup)
 	wg.Add(numCaches)
 
 	for name, c := range caches {
 		go func(name string, c *subCache) {
 			defer wg.Done()
-
 			for _, p := range ro.Paths {
 				// handle updates only
 				if !ro.UpdatesOnly {
@@ -330,6 +331,9 @@ func (gc *gnmiCache) handleOnChangeQuery(ctx context.Context, ro *ReadOpts, ch c
 				}
 				// main on-change subscription
 				fp := path.ToStrings(p, true)
+				fp = append(fp, "")
+				copy(fp[1:], fp)
+				fp[0] = ro.Target
 				// set callback
 				mc := &matchClient{name: name, ch: ch}
 				remove := c.match.AddQuery(fp, mc)
diff --git a/cmd/prompt.go b/cmd/prompt.go
index 1661b157..68993ec9 100644
--- a/cmd/prompt.go
+++ b/cmd/prompt.go
@@ -21,10 +21,11 @@ import (
 	homedir "github.com/mitchellh/go-homedir"
 	"github.com/nsf/termbox-go"
 	"github.com/olekukonko/tablewriter"
-	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/goyang/pkg/yang"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
+
+	"github.com/openconfig/gnmic/types"
 )
 
 var colorMapping = map[string]goprompt.Color{
@@ -298,13 +299,17 @@ func subscriptionTable(scs map[string]*types.SubscriptionConfig, list bool) [][]
 	if list {
 		tabData := make([][]string, 0, len(scs))
 		for _, sub := range scs {
+			enc := ""
+			if sub.Encoding != nil {
+				enc = *sub.Encoding
+			}
 			tabData = append(tabData, []string{
 				sub.Name,
 				sub.ModeString(),
 				sub.PrefixString(),
 				sub.PathsString(),
 				sub.SampleIntervalString(),
-				sub.Encoding,
+				enc,
 			})
 		}
 		sort.Slice(tabData, func(i, j int) bool {
@@ -323,7 +328,7 @@ func subscriptionTable(scs map[string]*types.SubscriptionConfig, list bool) [][]
 		tabData = append(tabData, []string{"Prefix", sub.PrefixString()})
 		tabData = append(tabData, []string{"Paths", sub.PathsString()})
 		tabData = append(tabData, []string{"Sample Interval", sub.SampleIntervalString()})
-		tabData = append(tabData, []string{"Encoding", sub.Encoding})
+		tabData = append(tabData, []string{"Encoding", *sub.Encoding})
 		tabData = append(tabData, []string{"Qos", sub.QosString()})
 		tabData = append(tabData, []string{"Heartbeat Interval", sub.HeartbeatIntervalString()})
 		return tabData
@@ -694,9 +699,11 @@ func subscriptionDescription(sub *types.SubscriptionConfig) string {
 			sb.WriteString(", ")
 		}
 	}
-	sb.WriteString("encoding=")
-	sb.WriteString(sub.Encoding)
-	sb.WriteString(", ")
+	if sub.Encoding != nil {
+		sb.WriteString("encoding=")
+		sb.WriteString(*sub.Encoding)
+		sb.WriteString(", ")
+	}
 	if sub.Prefix != "" {
 		sb.WriteString("prefix=")
 		sb.WriteString(sub.Prefix)
diff --git a/config/config.go b/config/config.go
index 4bac4d48..c9285a36 100644
--- a/config/config.go
+++ b/config/config.go
@@ -26,14 +26,15 @@ import (
 	"github.com/itchyny/gojq"
 	"github.com/mitchellh/go-homedir"
 	"github.com/openconfig/gnmi/proto/gnmi"
-	"github.com/openconfig/gnmic/api"
-	"github.com/openconfig/gnmic/types"
-	"github.com/openconfig/gnmic/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
 	"gopkg.in/natefinch/lumberjack.v2"
 	yaml "gopkg.in/yaml.v2"
+
+	"github.com/openconfig/gnmic/api"
+	"github.com/openconfig/gnmic/types"
+	"github.com/openconfig/gnmic/utils"
 )
 
 const (
diff --git a/config/diff.go b/config/diff.go
index 87ba2637..2650ffde 100644
--- a/config/diff.go
+++ b/config/diff.go
@@ -13,9 +13,10 @@ import (
 	"strings"
 
 	"github.com/openconfig/gnmi/proto/gnmi"
+	"github.com/spf13/cobra"
+
 	"github.com/openconfig/gnmic/api"
 	"github.com/openconfig/gnmic/types"
-	"github.com/spf13/cobra"
 )
 
 func (c *Config) CreateDiffSubscribeRequest(cmd *cobra.Command) (*gnmi.SubscribeRequest, error) {
@@ -26,12 +27,12 @@ func (c *Config) CreateDiffSubscribeRequest(cmd *cobra.Command) (*gnmi.Subscribe
 		Target:   c.DiffTarget,
 		Paths:    c.DiffPath,
 		Mode:     "ONCE",
-		Encoding: c.Encoding,
+		Encoding: &c.Encoding,
 	}
 	if flagIsSet(cmd, "qos") {
 		sc.Qos = &c.DiffQos
 	}
-	return c.CreateSubscribeRequest(sc, "")
+	return c.CreateSubscribeRequest(sc, nil)
 }
 
 func (c *Config) CreateDiffGetRequest() (*gnmi.GetRequest, error) {
diff --git a/config/subscriptions.go b/config/subscriptions.go
index 51fc4032..28858f38 100644
--- a/config/subscriptions.go
+++ b/config/subscriptions.go
@@ -17,11 +17,13 @@ import (
 	"strings"
 	"time"
 
+	"github.com/AlekSi/pointer"
 	"github.com/mitchellh/mapstructure"
 	"github.com/openconfig/gnmi/proto/gnmi"
+	"github.com/spf13/cobra"
+
 	"github.com/openconfig/gnmic/api"
 	"github.com/openconfig/gnmic/types"
-	"github.com/spf13/cobra"
 )
 
 const (
@@ -101,8 +103,11 @@ func (c *Config) subscriptionConfigFromFlags(cmd *cobra.Command) (map[string]*ty
 		SetTarget: c.LocalFlags.SubscribeSetTarget,
 		Paths:     c.LocalFlags.SubscribePath,
 		Mode:      c.LocalFlags.SubscribeMode,
-		Encoding:  c.Encoding,
 	}
+	// if globalFlagIsSet(cmd, "encoding") {
+	// 	sub.Encoding = &c.Encoding
+	// }
+	fmt.Println("!!", sub)
 	if flagIsSet(cmd, "qos") {
 		sub.Qos = &c.LocalFlags.SubscribeQos
 	}
@@ -161,12 +166,14 @@ func (c *Config) decodeSubscriptionConfig(sn string, s any, cmd *cobra.Command)
 		return nil, err
 	}
 	sub.Name = sn
-
+	fmt.Println("1", sub)
 	// inherit global "subscribe-*" option if it's not set
 	if err := c.setSubscriptionFieldsFromFlags(sub, cmd); err != nil {
 		return nil, err
 	}
+	fmt.Println("2", sub)
 	expandSubscriptionEnv(sub)
+	fmt.Println("3", sub)
 	return sub, nil
 }
 
@@ -177,9 +184,9 @@ func (c *Config) setSubscriptionFieldsFromFlags(sub *types.SubscriptionConfig, c
 	if sub.HeartbeatInterval == nil && flagIsSet(cmd, "heartbeat-interval") {
 		sub.HeartbeatInterval = &c.LocalFlags.SubscribeHeartbeatInterval
 	}
-	if sub.Encoding == "" {
-		sub.Encoding = c.Encoding
-	}
+	// if sub.Encoding == nil && globalFlagIsSet(cmd, "encoding") {
+	// 	sub.Encoding = &c.Encoding
+	// }
 	if sub.Mode == "" {
 		sub.Mode = c.LocalFlags.SubscribeMode
 	}
@@ -232,27 +239,36 @@ func (c *Config) GetSubscriptionsFromFile() []*types.SubscriptionConfig {
 	return subscriptions
 }
 
-func (c *Config) CreateSubscribeRequest(sc *types.SubscriptionConfig, target string) (*gnmi.SubscribeRequest, error) {
+func (c *Config) CreateSubscribeRequest(sc *types.SubscriptionConfig, tc *types.TargetConfig) (*gnmi.SubscribeRequest, error) {
 	err := validateAndSetDefaults(sc)
 	if err != nil {
 		return nil, err
 	}
-	gnmiOpts, err := c.subscriptionOpts(sc, target)
+	gnmiOpts, err := c.subscriptionOpts(sc, tc)
 	if err != nil {
 		return nil, err
 	}
 	return api.NewSubscribeRequest(gnmiOpts...)
 }
 
-func (c *Config) subscriptionOpts(sc *types.SubscriptionConfig, target string) ([]api.GNMIOption, error) {
+func (c *Config) subscriptionOpts(sc *types.SubscriptionConfig, tc *types.TargetConfig) ([]api.GNMIOption, error) {
 	gnmiOpts := make([]api.GNMIOption, 0, 4)
 
 	gnmiOpts = append(gnmiOpts,
 		api.Prefix(sc.Prefix),
-		api.Encoding(sc.Encoding),
 		api.SubscriptionListMode(sc.Mode),
 		api.UpdatesOnly(sc.UpdatesOnly),
 	)
+	// encoding
+	switch {
+	case sc.Encoding != nil:
+		gnmiOpts = append(gnmiOpts, api.Encoding(*sc.Encoding))
+	case tc != nil && tc.Encoding != nil:
+		gnmiOpts = append(gnmiOpts, api.Encoding(*tc.Encoding))
+	default:
+		gnmiOpts = append(gnmiOpts, api.Encoding(c.Encoding))
+	}
+
 	// history extension
 	if sc.History != nil {
 		if !sc.History.Snapshot.IsZero() {
@@ -276,13 +292,13 @@ func (c *Config) subscriptionOpts(sc *types.SubscriptionConfig, target string) (
 	if sc.Target != "" {
 		gnmiOpts = append(gnmiOpts, api.Target(sc.Target))
 	} else if sc.SetTarget {
-		gnmiOpts = append(gnmiOpts, api.Target(target))
+		gnmiOpts = append(gnmiOpts, api.Target(tc.Name))
 	}
 	// add gNMI subscriptions
 	// multiple stream subscriptions
 	if len(sc.StreamSubscriptions) > 0 {
 		for _, ssc := range sc.StreamSubscriptions {
-			streamGNMIOpts, err := c.streamSubscriptionOpts(ssc)
+			streamGNMIOpts, err := streamSubscriptionOpts(ssc)
 			if err != nil {
 				return nil, err
 			}
@@ -325,7 +341,7 @@ func (c *Config) subscriptionOpts(sc *types.SubscriptionConfig, target string) (
 	return gnmiOpts, nil
 }
 
-func (c *Config) streamSubscriptionOpts(sc *types.SubscriptionConfig) ([]api.GNMIOption, error) {
+func streamSubscriptionOpts(sc *types.SubscriptionConfig) ([]api.GNMIOption, error) {
 	gnmiOpts := make([]api.GNMIOption, 0)
 	for _, p := range sc.Paths {
 		subGnmiOpts := make([]api.GNMIOption, 0, 2)
@@ -380,19 +396,21 @@ func validateAndSetDefaults(sc *types.SubscriptionConfig) error {
 		return fmt.Errorf("%w: subscription %s: unknown subscription mode %q", ErrConfig, sc.Name, sc.Mode)
 	}
 	// validate encoding
-	switch strings.ToUpper(strings.ReplaceAll(sc.Encoding, "-", "_")) {
-	case "":
-		sc.Encoding = subscriptionDefaultEncoding
-	case "JSON":
-	case "BYTES":
-	case "PROTO":
-	case "ASCII":
-	case "JSON_IETF":
-	default:
-		// allow integer encoding values
-		_, err := strconv.Atoi(sc.Encoding)
-		if err != nil {
-			return fmt.Errorf("%w: subscription %s: unknown encoding type %q", ErrConfig, sc.Name, sc.Encoding)
+	if sc.Encoding != nil {
+		switch strings.ToUpper(strings.ReplaceAll(*sc.Encoding, "-", "_")) {
+		case "":
+			sc.Encoding = pointer.ToString(subscriptionDefaultEncoding)
+		case "JSON":
+		case "BYTES":
+		case "PROTO":
+		case "ASCII":
+		case "JSON_IETF":
+		default:
+			// allow integer encoding values
+			_, err := strconv.Atoi(*sc.Encoding)
+			if err != nil {
+				return fmt.Errorf("%w: subscription %s: unknown encoding type %q", ErrConfig, sc.Name, *sc.Encoding)
+			}
 		}
 	}
 
@@ -425,7 +443,7 @@ func validateAndSetDefaults(sc *types.SubscriptionConfig) error {
 			if scs.SetTarget {
 				return fmt.Errorf("%w: subscription %s/%d: 'set-target' attribute cannot be set", ErrConfig, sc.Name, i)
 			}
-			if scs.Encoding != "" {
+			if scs.Encoding != nil {
 				return fmt.Errorf("%w: subscription %s/%d: 'encoding' attribute cannot be set", ErrConfig, sc.Name, i)
 			}
 			if scs.History != nil {
@@ -490,5 +508,7 @@ func expandSubscriptionEnv(sc *types.SubscriptionConfig) {
 	}
 	sc.Mode = os.ExpandEnv(sc.Mode)
 	sc.StreamMode = os.ExpandEnv(sc.StreamMode)
-	sc.Encoding = os.ExpandEnv(sc.Encoding)
+	if sc.Encoding != nil {
+		sc.Encoding = pointer.ToString(os.ExpandEnv(*sc.Encoding))
+	}
 }
diff --git a/config/subscriptions_test.go b/config/subscriptions_test.go
index 91d14716..528ebbc0 100644
--- a/config/subscriptions_test.go
+++ b/config/subscriptions_test.go
@@ -22,10 +22,11 @@ import (
 	"github.com/AlekSi/pointer"
 	"github.com/openconfig/gnmi/proto/gnmi"
 	"github.com/openconfig/gnmi/proto/gnmi_ext"
-	"github.com/openconfig/gnmic/testutils"
-	"github.com/openconfig/gnmic/types"
 	"github.com/spf13/viper"
 	"google.golang.org/protobuf/encoding/prototext"
+
+	"github.com/openconfig/gnmic/testutils"
+	"github.com/openconfig/gnmic/types"
 )
 
 func mustParseTime(tm string) time.Time {
@@ -58,23 +59,23 @@ subscriptions:
 		},
 		outErr: nil,
 	},
-	"with_globals": {
-		in: []byte(`
-encoding: proto
-subscriptions:
-  sub1:
-    paths: 
-      - /valid/path
-`),
-		out: map[string]*types.SubscriptionConfig{
-			"sub1": {
-				Name:     "sub1",
-				Paths:    []string{"/valid/path"},
-				Encoding: "proto",
-			},
-		},
-		outErr: nil,
-	},
+	// 	"with_globals": {
+	// 		in: []byte(`
+	// subscribe-sample-interval: 10s
+	// subscriptions:
+	//   sub1:
+	//     paths:
+	//       - /valid/path
+	// `),
+	// 		out: map[string]*types.SubscriptionConfig{
+	// 			"sub1": {
+	// 				Name:           "sub1",
+	// 				Paths:          []string{"/valid/path"},
+	// 				SampleInterval: pointer.ToDuration(10 * time.Second),
+	// 			},
+	// 		},
+	// 		outErr: nil,
+	// 	},
 	"2_subs": {
 		in: []byte(`
 subscriptions:
@@ -101,42 +102,41 @@ subscriptions:
 		},
 		outErr: nil,
 	},
-	"2_subs_with_globals": {
-		in: []byte(`
-encoding: proto
-subscriptions:
-  sub1:
-    paths: 
-      - /valid/path
-  sub2:
-    paths: 
-      - /valid/path2
-    mode: stream
-    stream-mode: on_change
-`),
-		out: map[string]*types.SubscriptionConfig{
-			"sub1": {
-				Name:     "sub1",
-				Paths:    []string{"/valid/path"},
-				Encoding: "proto",
-			},
-			"sub2": {
-				Name:       "sub2",
-				Paths:      []string{"/valid/path2"},
-				Mode:       "stream",
-				StreamMode: "on_change",
-				Encoding:   "proto",
-			},
-		},
-		outErr: nil,
-	},
+	// 	"2_subs_with_globals": {
+	// 		in: []byte(`
+	// subscribe-sample-interval: 10s
+	// subscriptions:
+	//   sub1:
+	//     paths:
+	//       - /valid/path
+	//   sub2:
+	//     paths:
+	//       - /valid/path2
+	//     mode: stream
+	//     stream-mode: on_change
+	// `),
+	// 		out: map[string]*types.SubscriptionConfig{
+	// 			"sub1": {
+	// 				Name:           "sub1",
+	// 				Paths:          []string{"/valid/path"},
+	// 				SampleInterval: pointer.ToDuration(10 * time.Second),
+	// 			},
+	// 			"sub2": {
+	// 				Name:           "sub2",
+	// 				Paths:          []string{"/valid/path2"},
+	// 				Mode:           "stream",
+	// 				StreamMode:     "on_change",
+	// 				SampleInterval: pointer.ToDuration(10 * time.Second),
+	// 			},
+	// 		},
+	// 		outErr: nil,
+	// 	},
 	"3_subs_with_env": {
 		envs: []string{
 			"SUB1_PATH=/valid/path",
 			"SUB2_PATH=/valid/path2",
 		},
 		in: []byte(`
-encoding: proto
 subscriptions:
   sub1:
     paths: 
@@ -149,16 +149,14 @@ subscriptions:
 `),
 		out: map[string]*types.SubscriptionConfig{
 			"sub1": {
-				Name:     "sub1",
-				Paths:    []string{"/valid/path"},
-				Encoding: "proto",
+				Name:  "sub1",
+				Paths: []string{"/valid/path"},
 			},
 			"sub2": {
 				Name:       "sub2",
 				Paths:      []string{"/valid/path2"},
 				Mode:       "stream",
 				StreamMode: "on_change",
-				Encoding:   "proto",
 			},
 		},
 		outErr: nil,
@@ -295,7 +293,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 	}
 	type args struct {
 		sc     *types.SubscriptionConfig
-		target string
+		target *types.TargetConfig
 	}
 	tests := []struct {
 		name    string
@@ -312,7 +310,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"interface",
 					},
 					Mode:     "once",
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 				},
 			},
 			want: &gnmi.SubscribeRequest{
@@ -343,7 +341,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"network-instance",
 					},
 					Mode:     "once",
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 				},
 			},
 			want: &gnmi.SubscribeRequest{
@@ -380,7 +378,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"interface",
 					},
 					Mode:     "poll",
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 				},
 			},
 			want: &gnmi.SubscribeRequest{
@@ -411,7 +409,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"network-instance",
 					},
 					Mode:     "poll",
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 				},
 			},
 			want: &gnmi.SubscribeRequest{
@@ -448,7 +446,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"interface",
 					},
 					Mode:     "stream",
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 				},
 			},
 			want: &gnmi.SubscribeRequest{
@@ -479,7 +477,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"network-instance",
 					},
 					Mode:     "stream",
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 				},
 			},
 			want: &gnmi.SubscribeRequest{
@@ -516,7 +514,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"interface",
 					},
 					StreamMode:     "sample",
-					Encoding:       "json_ietf",
+					Encoding:       pointer.ToString("json_ietf"),
 					SampleInterval: pointer.ToDuration(5 * time.Second),
 				},
 			},
@@ -547,7 +545,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"interface",
 					},
 					StreamMode: "on-change",
-					Encoding:   "json_ietf",
+					Encoding:   pointer.ToString("json_ietf"),
 				},
 			},
 			want: &gnmi.SubscribeRequest{
@@ -577,7 +575,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"interface",
 					},
 					StreamMode: "on_change",
-					Encoding:   "json_ietf",
+					Encoding:   pointer.ToString("json_ietf"),
 				},
 			},
 			want: &gnmi.SubscribeRequest{
@@ -607,7 +605,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 						"interface",
 					},
 					Mode:     "once",
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 					History: &types.HistoryConfig{
 						Snapshot: mustParseTime("2022-07-14T07:30:00.0Z"),
 					},
@@ -647,7 +645,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 			name: "combined_on-change_and_sample",
 			args: args{
 				sc: &types.SubscriptionConfig{
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 					StreamSubscriptions: []*types.SubscriptionConfig{
 						{
 							Paths: []string{
@@ -705,7 +703,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 			name: "combined_on-change_and_sample_multiple_paths",
 			args: args{
 				sc: &types.SubscriptionConfig{
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 					StreamSubscriptions: []*types.SubscriptionConfig{
 						{
 							Paths: []string{
@@ -795,7 +793,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 			args: args{
 				sc: &types.SubscriptionConfig{
 					Paths:    []string{"network-instance"},
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 					StreamSubscriptions: []*types.SubscriptionConfig{
 						{
 							Paths: []string{
@@ -818,7 +816,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 			name: "invalid_combined_subscriptions_mode",
 			args: args{
 				sc: &types.SubscriptionConfig{
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 					StreamSubscriptions: []*types.SubscriptionConfig{
 						{
 							Paths: []string{
@@ -841,7 +839,7 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 			name: "invalid_subscription mode",
 			args: args{
 				sc: &types.SubscriptionConfig{
-					Encoding: "json_ietf",
+					Encoding: pointer.ToString("json_ietf"),
 					Mode:     "ONCE",
 					StreamSubscriptions: []*types.SubscriptionConfig{
 						{
@@ -861,6 +859,70 @@ func TestConfig_CreateSubscribeRequest(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "encoding_from_target",
+			args: args{
+				sc: &types.SubscriptionConfig{
+					Paths: []string{
+						"interface",
+					},
+					Mode: "once",
+				},
+				target: &types.TargetConfig{
+					Encoding: pointer.ToString("json_ietf"),
+				},
+			},
+			want: &gnmi.SubscribeRequest{
+				Request: &gnmi.SubscribeRequest_Subscribe{
+					Subscribe: &gnmi.SubscriptionList{
+						Subscription: []*gnmi.Subscription{
+							{
+								Path: &gnmi.Path{
+									Elem: []*gnmi.PathElem{{
+										Name: "interface",
+									}},
+								},
+							},
+						},
+						Mode:     gnmi.SubscriptionList_ONCE,
+						Encoding: gnmi.Encoding_JSON_IETF,
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "encoding_from_global",
+			fields: fields{
+				GlobalFlags: GlobalFlags{Encoding: "json_ietf"},
+			},
+			args: args{
+				sc: &types.SubscriptionConfig{
+					Paths: []string{
+						"interface",
+					},
+					Mode: "once",
+				},
+			},
+			want: &gnmi.SubscribeRequest{
+				Request: &gnmi.SubscribeRequest_Subscribe{
+					Subscribe: &gnmi.SubscriptionList{
+						Subscription: []*gnmi.Subscription{
+							{
+								Path: &gnmi.Path{
+									Elem: []*gnmi.PathElem{{
+										Name: "interface",
+									}},
+								},
+							},
+						},
+						Mode:     gnmi.SubscriptionList_ONCE,
+						Encoding: gnmi.Encoding_JSON_IETF,
+					},
+				},
+			},
+			wantErr: false,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/docs/user_guide/event_processors/event_combine.md b/docs/user_guide/event_processors/event_combine.md
new file mode 100644
index 00000000..2d8e5914
--- /dev/null
+++ b/docs/user_guide/event_processors/event_combine.md
@@ -0,0 +1,98 @@
+The `event-combine` processor combines multiple processors together. 
+This allows to declare processors once and reuse them to build more complex processors.
+
+### Configuration
+
+```yaml
+processors:
+  # processor name
+  pipeline1:
+    # processor type
+    event-combine:
+      # list of regex to be matched with the values names
+      processors: 
+          # The "sub" processor execution condition. A jq expression.
+        - condition: 
+          # the processor name, should be declared in the
+          # `processors` section.
+          name: 
+      # enable extra logging
+      debug: false
+```
+
+### Conditional Execution of Subprocessors
+
+The workflow for processing event messages can include multiple subprocessors, each potentially governed by its own condition. These conditions are defined using the jq query language, enabling dynamic and precise control over when each subprocessor should be executed.
+
+### Defining Conditions for Subprocessors
+
+When configuring your subprocessors, you have the option to attach a jq-based condition to each one. The specified condition acts as a gatekeeper, determining whether the corresponding subprocessor should be activated for a particular event message.
+
+### Condition Evaluation Process
+
+For a subprocessor to run, the following criteria must be met:
+
+Condition Presence: If a condition is specified for the subprocessor, it must be evaluated.
+
+Condition Outcome: The result of the jq condition evaluation must be true.
+
+Combined Conditions: In scenarios where both the main processor and the subprocessor have associated conditions, both conditions must independently evaluate to true for the subprocessor to be triggered.
+
+Only when all relevant conditions are met will the subprocessor execute its designated operations on the event message.
+
+It is important to note that the absence of a condition is equivalent to a condition that always evaluates to true. Thus, if no condition is provided for a subprocessor, it will execute as long as the main processor's condition (if any) is met.
+
+By using conditional execution, you can build sophisticated and efficient event message processing workflows that react dynamically to the content of the messages.
+
+### Examples
+
+In the below example, we define 3 regular processors and 2 `event-combine` processors.
+
+- `proc1`: Allows event message that have tag `"interface_name = ethernet-1/1`
+
+- `proc2`: Renames values names to their path base.
+             e.g: `interface/statistics/out-octets` --> `out-octets`
+
+- `proc3`: Converts any values with a name ending with `octets` to `int`.
+
+- `pipeline1`: Combines `proc1`, `proc2` and `proc3`, applying `proc2` only to subscription `sub1`
+
+- `pipeline2`: Combines `proc2` and `proc3`, applying `proc2` only to subscription `sub2`
+
+The 2 combine processors can be linked with different outputs.
+
+```yaml
+processors:
+  proc1:
+    event-allow:
+      condition: '.tags.interface_name == "ethernet-1/1"'
+
+  proc2:
+    event-strings:
+      value-names:
+        - ".*"
+      transforms:
+        - path-base:
+            apply-on: "name"
+  proc3:
+    event-convert:
+      value-names: 
+        - ".*octets$"
+      type: int 
+  
+
+  pipeline1:
+    event-combine:
+      processors: 
+        - name: proc1
+        - condition: '.tags["subscription-name"] == "sub1"'
+          name: proc2
+        - name: proc3
+  
+  pipeline2:
+    event-combine:
+      processors: 
+        - condition: '.tags["subscription-name"] == "sub2"'
+          name: proc2
+        - name: proc3
+```
diff --git a/docs/user_guide/event_processors/event_rate_limit.md b/docs/user_guide/event_processors/event_rate_limit.md
new file mode 100644
index 00000000..f3990b4c
--- /dev/null
+++ b/docs/user_guide/event_processors/event_rate_limit.md
@@ -0,0 +1,28 @@
+The `event-rate-limit` processor rate-limits each event with matching tags to the configured amount per-seconds.
+
+All the tags for each event is hashed, and if the hash matches a previously seen event, then the timestamp 
+of the event itself is compared to assess if the configured limit has been exceeded.
+If it has, then this new event is dropped from the pipeline.
+
+The cache for comparing timestamp is an LRU cache, with a default size of 1000 that can be increased for bigger deployments.
+
+To account for cases where the device will artificially split the event into multiple chunks (with the same timestamp), 
+the rate-limiter will ignore events with exactly the same timestamp.
+
+
+### Examples
+
+```yaml
+processors:
+  # processor name
+  rate-limit-100pps:
+    # processor type
+    event-rate-limit:
+      # rate of filtering, in events per seconds
+      per-second: 100
+      # set the cache size for doing the rate-limiting comparison
+      # default value is 1000
+      cache-size: 10000
+      # debug for additionnal logging of dropped events
+      debug: true
+```
\ No newline at end of file
diff --git a/docs/user_guide/outputs/asciigraph_output.md b/docs/user_guide/outputs/asciigraph_output.md
new file mode 100644
index 00000000..ea522911
--- /dev/null
+++ b/docs/user_guide/outputs/asciigraph_output.md
@@ -0,0 +1,155 @@
+`gnmic` supports displaying collected metrics as an ASCII graph on the terminal.
+The graph is generated using the [asciigraph](https://github.com/guptarohit/asciigraph) package.
+
+### Configuration sample
+
+```yaml
+
+outputs:
+  output1:
+    # required
+    type: asciigraph
+    # string, the graph caption
+    caption: 
+    # integer, the graph height. If unset, defaults to the terminal height
+    height:
+    # integer, the graph width. If unset, defaults to the terminal width
+    width:
+    # float, the graph minimum value for the vertical axis.
+    lower-bound:
+    # float, the graph minimum value for the vertical axis.
+    upper-bound:
+    # integer, the graph left offset.
+    offset:
+    # integer, the decimal point precision of the label values.
+    precision:
+    # string, the caption color. one of ANSI colors.
+    caption-color:
+    # string, the axis color. one of ANSI colors.
+    axis-color:
+    # string, the label color. one of ANSI colors.
+    label-color:
+    # duration, the graph refresh timer.
+    refresh-timer: 1s
+    # string, one of `overwrite`, `if-not-present`, ``
+    # This field allows populating/changing the value of Prefix.Target in the received message.
+    # if set to ``, nothing changes 
+    # if set to `overwrite`, the target value is overwritten using the template configured under `target-template`
+    # if set to `if-not-present`, the target value is populated only if it is empty, still using the `target-template`
+    add-target: 
+    # string, a GoTemplate that allows for the customization of the target field in Prefix.Target.
+    # it applies only if the previous field `add-target` is not empty.
+    # if left empty, it defaults to:
+    # {{- if index . "subscription-target" -}}
+    # {{ index . "subscription-target" }}
+    # {{- else -}}
+    # {{ index . "source" | host }}
+    # {{- end -}}`
+    # which will set the target to the value configured under `subscription.$subscription-name.target` if any,
+    # otherwise it will set it to the target name stripped of the port number (if present)
+    target-template:
+    # list of processors to apply on the message before writing
+    event-processors: 
+    # bool enable debug
+    debug: false 
+```
+
+### Example
+
+This example shows how to use the `asciigraph` output.
+
+gNMIc config
+
+```shell
+cat gnmic_asciiout.yaml
+```
+
+```yaml
+targets:
+  clab-nfd33-spine1-1:
+    username: admin
+    password: NokiaSrl1!
+    skip-verify: true
+
+subscriptions:
+  sub1:
+    paths:
+      - /interface[name=ethernet-1/3]/statistics/out-octets
+      - /interface[name=ethernet-1/3]/statistics/in-octets
+    stream-mode: sample
+    sample-interval: 1s
+    encoding: ascii
+
+outputs:
+  out1:
+    type: asciigraph
+    caption: in/out octets per second
+    event-processors:
+      - rate
+
+processors:
+  rate:
+    event-starlark:
+      script: rate.star
+```
+
+Starlark processor
+
+```shell
+cat rate.star
+```
+
+```python
+cache = {}
+
+values_names = [
+  '/interface/statistics/out-octets',
+  '/interface/statistics/in-octets'
+]
+
+N=2
+
+def apply(*events):
+  for e in events:
+    for value_name in values_names:
+      v = e.values.get(value_name)
+      # check if v is not None and is a digit to proceed
+      if not v:
+        continue
+      if not v.isdigit():
+        continue
+      # update cache with the latest value
+      val_key = "_".join([e.tags["source"], e.tags["interface_name"], value_name])
+      if not cache.get(val_key):
+        # initialize the cache entry if empty
+        cache.update({val_key: []})
+      if len(cache[val_key]) > N:
+        # remove the oldest entry if the number of entries reached N
+        cache[val_key] = cache[val_key][1:]
+      # update cache entry
+      cache[val_key].append((int(v), e.timestamp))
+      # get the list of values
+      val_list = cache[val_key]
+      # calculate rate
+      e.values[value_name+"_rate"] = rate(val_list)
+      e.values.pop(value_name)
+    
+  return events
+
+def rate(vals):
+  previous_value, previous_timestamp = None, None
+  for value, timestamp in vals:
+    if previous_value != None and previous_timestamp != None:
+      time_diff = (timestamp - previous_timestamp) / 1000000000 # 1 000 000 000
+      if time_diff > 0:
+        value_diff = value - previous_value
+        rate = value_diff / time_diff
+        return rate
+
+    previous_value = value
+    previous_timestamp = timestamp
+
+  return 0
+```
+
+<script async id="asciicast-617477" src="https://asciinema.org/a/617477.js"></script>
diff --git a/docs/user_guide/outputs/prometheus_write_output.md b/docs/user_guide/outputs/prometheus_write_output.md
index 5c11fc70..ad466bc4 100644
--- a/docs/user_guide/outputs/prometheus_write_output.md
+++ b/docs/user_guide/outputs/prometheus_write_output.md
@@ -14,6 +14,7 @@ outputs:
     # a map of string:string, 
     # custom HTTP headers to be sent along with each remote write request.
     headers:
+      # header: value
     # sets the `Authorization` header on every remote write request with the
     # configured username and password.
     authentication:
@@ -92,6 +93,8 @@ outputs:
     event-processors: 
     # an integer, sets the number of worker handling messages to be converted into Prometheus metrics
     num-workers: 1
+    # an integer, sets the number of writers draining the buffer and writing to Prometheus
+    num-writers: 1
 ```
 
 `gnmic` creates the prometheus metric name and its labels from the subscription name, the gnmic path and the value name.
diff --git a/docs/user_guide/targets/targets.md b/docs/user_guide/targets/targets.md
index 753f48ed..86c4e235 100644
--- a/docs/user_guide/targets/targets.md
+++ b/docs/user_guide/targets/targets.md
@@ -149,6 +149,11 @@ targets:
     # if empty it defaults to all subscriptions defined under
     # the main level `subscriptions` field
     subscriptions:
+    # string, case insensitive, defines the gNMI encoding to be used for 
+    # the subscriptions to be established for this target.
+    # This encoding value applies only if the subscription configuration does
+    # NOT explicitly define an encoding.
+    encoding:
     # list of output names to which the gnmi data will be written.
     # if empty if defaults to all outputs defined under
     # the main level `outputs` field
diff --git a/formatters/all/all.go b/formatters/all/all.go
index 9f6e0df2..4b7e9d8b 100644
--- a/formatters/all/all.go
+++ b/formatters/all/all.go
@@ -11,6 +11,7 @@ package all
 import (
 	_ "github.com/openconfig/gnmic/formatters/event_add_tag"
 	_ "github.com/openconfig/gnmic/formatters/event_allow"
+	_ "github.com/openconfig/gnmic/formatters/event_combine"
 	_ "github.com/openconfig/gnmic/formatters/event_convert"
 	_ "github.com/openconfig/gnmic/formatters/event_data_convert"
 	_ "github.com/openconfig/gnmic/formatters/event_date_string"
@@ -22,6 +23,7 @@ import (
 	_ "github.com/openconfig/gnmic/formatters/event_jq"
 	_ "github.com/openconfig/gnmic/formatters/event_merge"
 	_ "github.com/openconfig/gnmic/formatters/event_override_ts"
+	_ "github.com/openconfig/gnmic/formatters/event_rate_limit"
 	_ "github.com/openconfig/gnmic/formatters/event_starlark"
 	_ "github.com/openconfig/gnmic/formatters/event_strings"
 	_ "github.com/openconfig/gnmic/formatters/event_to_tag"
diff --git a/formatters/event_add_tag/event_add_tag.go b/formatters/event_add_tag/event_add_tag.go
index 393f7dc2..e80ee5b9 100644
--- a/formatters/event_add_tag/event_add_tag.go
+++ b/formatters/event_add_tag/event_add_tag.go
@@ -17,6 +17,7 @@ import (
 	"strings"
 
 	"github.com/itchyny/gojq"
+
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
@@ -27,8 +28,8 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-// AddTag adds a set of tags to the event message if tag
-type AddTag struct {
+// addTag adds a set of tags to the event message if tag
+type addTag struct {
 	Condition  string            `mapstructure:"condition,omitempty"`
 	Tags       []string          `mapstructure:"tags,omitempty" json:"tags,omitempty"`
 	Values     []string          `mapstructure:"values,omitempty" json:"values,omitempty"`
@@ -48,13 +49,13 @@ type AddTag struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &AddTag{
+		return &addTag{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (p *AddTag) Init(cfg interface{}, opts ...formatters.Option) error {
+func (p *addTag) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, p)
 	if err != nil {
 		return err
@@ -121,7 +122,7 @@ func (p *AddTag) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (p *AddTag) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (p *addTag) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	for _, e := range es {
 		if e == nil {
 			continue
@@ -172,7 +173,7 @@ func (p *AddTag) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (p *AddTag) WithLogger(l *log.Logger) {
+func (p *addTag) WithLogger(l *log.Logger) {
 	if p.Debug && l != nil {
 		p.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if p.Debug {
@@ -180,11 +181,13 @@ func (p *AddTag) WithLogger(l *log.Logger) {
 	}
 }
 
-func (p *AddTag) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (p *addTag) WithTargets(tcs map[string]*types.TargetConfig) {}
+
+func (p *addTag) WithActions(act map[string]map[string]interface{}) {}
 
-func (p *AddTag) WithActions(act map[string]map[string]interface{}) {}
+func (p *addTag) WithProcessors(procs map[string]map[string]any) {}
 
-func (p *AddTag) addTags(e *formatters.EventMsg) {
+func (p *addTag) addTags(e *formatters.EventMsg) {
 	if e.Tags == nil {
 		e.Tags = make(map[string]string)
 	}
diff --git a/formatters/event_allow/event_allow.go b/formatters/event_allow/event_allow.go
index 187935d7..af634e4d 100644
--- a/formatters/event_allow/event_allow.go
+++ b/formatters/event_allow/event_allow.go
@@ -17,6 +17,7 @@ import (
 	"strings"
 
 	"github.com/itchyny/gojq"
+
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
@@ -27,8 +28,8 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-// Allow Allows the msg if ANY of the Tags or Values regexes are matched
-type Allow struct {
+// allow Allows the msg if ANY of the Tags or Values regexes are matched
+type allow struct {
 	Condition  string   `mapstructure:"condition,omitempty"`
 	TagNames   []string `mapstructure:"tag-names,omitempty" json:"tag-names,omitempty"`
 	ValueNames []string `mapstructure:"value-names,omitempty" json:"value-names,omitempty"`
@@ -46,13 +47,13 @@ type Allow struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &Allow{
+		return &allow{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (d *Allow) Init(cfg interface{}, opts ...formatters.Option) error {
+func (d *allow) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, d)
 	if err != nil {
 		return err
@@ -115,7 +116,7 @@ func (d *Allow) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (d *Allow) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (d *allow) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	i := 0
 	for _, e := range es {
 		if d.allow(e) {
@@ -130,7 +131,7 @@ func (d *Allow) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (d *Allow) WithLogger(l *log.Logger) {
+func (d *allow) WithLogger(l *log.Logger) {
 	if d.Debug && l != nil {
 		d.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if d.Debug {
@@ -138,11 +139,13 @@ func (d *Allow) WithLogger(l *log.Logger) {
 	}
 }
 
-func (d *Allow) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (d *allow) WithTargets(tcs map[string]*types.TargetConfig) {}
+
+func (d *allow) WithActions(act map[string]map[string]interface{}) {}
 
-func (d *Allow) WithActions(act map[string]map[string]interface{}) {}
+func (d *allow) WithProcessors(procs map[string]map[string]any) {}
 
-func (d *Allow) allow(e *formatters.EventMsg) bool {
+func (d *allow) allow(e *formatters.EventMsg) bool {
 	if d.Condition != "" {
 		ok, err := formatters.CheckCondition(d.code, e)
 		if err != nil {
diff --git a/formatters/event_combine/event_combine.go b/formatters/event_combine/event_combine.go
new file mode 100644
index 00000000..7fdf7837
--- /dev/null
+++ b/formatters/event_combine/event_combine.go
@@ -0,0 +1,184 @@
+// © 2023 Nokia.
+//
+// This code is a Contribution to the gNMIc project (“Work”) made under the Google Software Grant and Corporate Contributor License Agreement (“CLA”) and governed by the Apache License 2.0.
+// No other rights or licenses in or to any of Nokia’s intellectual property are granted for any other purpose.
+// This code is provided on an “as is” basis without any warranties of any kind.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package event_combine_test
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"sort"
+	"strings"
+
+	"github.com/itchyny/gojq"
+
+	"github.com/openconfig/gnmic/formatters"
+	"github.com/openconfig/gnmic/types"
+	"github.com/openconfig/gnmic/utils"
+)
+
+const (
+	processorType = "event-combine"
+	loggingPrefix = "[" + processorType + "] "
+)
+
+// combine allows running multiple processors together based on conditions
+type combine struct {
+	Processors []*procseq `mapstructure:"processors,omitempty"`
+	Debug      bool       `mapstructure:"debug,omitempty"`
+
+	processorsDefinitions map[string]map[string]any
+	targetsConfigs        map[string]*types.TargetConfig
+	actionsDefinitions    map[string]map[string]any
+
+	logger *log.Logger
+}
+
+type procseq struct {
+	Condition string `mapstructure:"condition,omitempty"`
+	Name      string `mapstructure:"name,omitempty"`
+
+	condition *gojq.Code
+	proc      formatters.EventProcessor
+}
+
+func init() {
+	formatters.Register(processorType, func() formatters.EventProcessor {
+		return &combine{
+			logger: log.New(io.Discard, "", 0),
+		}
+	})
+}
+
+func (p *combine) Init(cfg any, opts ...formatters.Option) error {
+	err := formatters.DecodeConfig(cfg, p)
+	if err != nil {
+		return err
+	}
+	for _, opt := range opts {
+		opt(p)
+	}
+	if len(p.Processors) == 0 {
+		return fmt.Errorf("missing processors definition")
+	}
+	for i, proc := range p.Processors {
+		if proc == nil {
+			return fmt.Errorf("missing processor(#%d) definition", i)
+		}
+		if proc.Name == "" {
+			return fmt.Errorf("invalid processor(#%d) definition: missing name", i)
+		}
+		// init condition if it's set
+		if proc.Condition != "" {
+			proc.Condition = strings.TrimSpace(proc.Condition)
+			q, err := gojq.Parse(proc.Condition)
+			if err != nil {
+				return err
+			}
+			proc.condition, err = gojq.Compile(q)
+			if err != nil {
+				return err
+			}
+		}
+		// init subprocessors
+		if epCfg, ok := p.processorsDefinitions[proc.Name]; ok {
+			epType := ""
+			for k := range epCfg {
+				epType = k
+				break
+			}
+			if in, ok := formatters.EventProcessors[epType]; ok {
+				proc.proc = in()
+				err := proc.proc.Init(epCfg[epType],
+					formatters.WithLogger(p.logger),
+					formatters.WithTargets(p.targetsConfigs),
+					formatters.WithActions(p.actionsDefinitions),
+					formatters.WithProcessors(p.actionsDefinitions),
+				)
+				if err != nil {
+					return fmt.Errorf("failed initializing event processor '%s' of type='%s': %v", proc.Name, epType, err)
+				}
+				p.logger.Printf("added event processor '%s' of type=%s to combine processor", proc.Name, epType)
+				continue
+			}
+			return fmt.Errorf("%q event processor has an unknown type=%q", proc.Name, epType)
+		}
+		return fmt.Errorf("%q event processor not found", proc.Name)
+	}
+	if p.logger.Writer() != io.Discard {
+		b, err := json.Marshal(p)
+		if err != nil {
+			p.logger.Printf("initialized processor '%s': %+v", processorType, p)
+			return nil
+		}
+		p.logger.Printf("initialized processor '%s': %s", processorType, string(b))
+	}
+	return nil
+}
+
+func (p *combine) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+	les := len(es)
+
+	in := make([]*formatters.EventMsg, 0, les)
+	out := make([]*formatters.EventMsg, 0, les)
+	for _, proc := range p.Processors {
+		in = in[:0]
+		out = out[:0]
+
+		for i, e := range es {
+			ok, err := formatters.CheckCondition(proc.condition, e)
+			if err != nil {
+				p.logger.Printf("condition check failed: %v", err)
+			}
+			if ok {
+				if p.Debug {
+					p.logger.Printf("processor #%d include: %s", i, e)
+				}
+				in = append(in, e)
+				continue
+			}
+			if p.Debug {
+				p.logger.Printf("processor #%d exclude: %s", i, e)
+			}
+			out = append(out, e)
+		}
+
+		in = proc.proc.Apply(in...)
+		es = es[:0]
+		es = append(es, in...)
+		es = append(es, out...)
+		if len(es) > 1 {
+			sort.Slice(es, func(i, j int) bool {
+				return es[i].Timestamp < es[j].Timestamp
+			})
+		}
+	}
+	return es
+}
+
+func (s *combine) WithLogger(l *log.Logger) {
+	if s.Debug && l != nil {
+		s.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
+	} else if s.Debug {
+		s.logger = log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags)
+	}
+}
+
+func (s *combine) WithTargets(tcs map[string]*types.TargetConfig) {
+	s.targetsConfigs = tcs
+}
+
+func (s *combine) WithActions(act map[string]map[string]any) {
+	s.actionsDefinitions = act
+}
+
+func (s *combine) WithProcessors(procs map[string]map[string]any) {
+	s.processorsDefinitions = procs
+}
diff --git a/formatters/event_combine/event_combine_test/event_combine_test.go b/formatters/event_combine/event_combine_test/event_combine_test.go
new file mode 100644
index 00000000..7c3363bc
--- /dev/null
+++ b/formatters/event_combine/event_combine_test/event_combine_test.go
@@ -0,0 +1,190 @@
+// © 2023 Nokia.
+//
+// This code is a Contribution to the gNMIc project (“Work”) made under the Google Software Grant and Corporate Contributor License Agreement (“CLA”) and governed by the Apache License 2.0.
+// No other rights or licenses in or to any of Nokia’s intellectual property are granted for any other purpose.
+// This code is provided on an “as is” basis without any warranties of any kind.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package event_sequence
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/openconfig/gnmic/formatters"
+	_ "github.com/openconfig/gnmic/formatters/all"
+)
+
+func Test_combine_Apply(t *testing.T) {
+	type fields struct {
+		processorConfig map[string]any
+		processorsSet   map[string]map[string]any
+	}
+	type args struct {
+		es []*formatters.EventMsg
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+		want   []*formatters.EventMsg
+	}{
+		{
+			name: "simple1",
+			fields: fields{
+				processorConfig: map[string]any{
+					"debug": true,
+					"processors": []any{
+						map[string]any{
+							"condition": ".tags.tag == \"t1\"",
+							"name":      "proc1",
+						},
+						map[string]any{
+							"name": "proc2",
+						},
+					},
+				},
+				processorsSet: map[string]map[string]any{
+					"proc1": {
+						"event-strings": map[string]any{
+							"value-names": []string{"^number$"},
+							"transforms": []map[string]any{
+								{
+									"replace": map[string]any{
+										"apply-on": "name",
+										"old":      "number",
+										"new":      "new_number",
+									},
+								},
+							},
+							"debug": true,
+						},
+					},
+					"proc2": {
+						"event-strings": map[string]any{
+							"tag-names": []string{"^tag$"},
+							"transforms": []map[string]any{
+								{
+									"replace": map[string]any{
+										"apply-on": "name",
+										"old":      "tag",
+										"new":      "new_tag",
+									},
+								},
+							},
+							"debug": true,
+						},
+					},
+				},
+			},
+			args: args{
+				es: []*formatters.EventMsg{
+					{
+						Tags:   map[string]string{"tag": "t1"},
+						Values: map[string]interface{}{"number": "42"},
+					},
+					{
+						Tags:   map[string]string{"t": "t1"},
+						Values: map[string]interface{}{"n": "42"},
+					},
+				},
+			},
+			want: []*formatters.EventMsg{
+				{
+					Tags:   map[string]string{"new_tag": "t1"},
+					Values: map[string]interface{}{"new_number": "42"},
+				},
+				{
+					Tags:   map[string]string{"t": "t1"},
+					Values: map[string]interface{}{"n": "42"},
+				},
+			},
+		},
+		{
+			name: "simple2",
+			fields: fields{
+				processorConfig: map[string]any{
+					"debug": true,
+					"processors": []any{
+						map[string]any{
+							"condition": ".tags.tag == \"t2\"",
+							"name":      "proc1",
+						},
+						map[string]any{
+							"name": "proc2",
+						},
+					},
+				},
+				processorsSet: map[string]map[string]any{
+					"proc1": {
+						"event-strings": map[string]any{
+							"value-names": []string{"^number$"},
+							"transforms": []map[string]any{
+								{
+									"replace": map[string]any{
+										"apply-on": "name",
+										"old":      "number",
+										"new":      "new_number",
+									},
+								},
+							},
+							"debug": true,
+						},
+					},
+					"proc2": {
+						"event-strings": map[string]any{
+							"tag-names": []string{"^tag$"},
+							"transforms": []map[string]any{
+								{
+									"replace": map[string]any{
+										"apply-on": "name",
+										"old":      "tag",
+										"new":      "new_tag",
+									},
+								},
+							},
+							"debug": true,
+						},
+					},
+				},
+			},
+			args: args{
+				es: []*formatters.EventMsg{
+					{
+						Tags:   map[string]string{"tag": "t1"},
+						Values: map[string]interface{}{"number": "42"},
+					},
+					{
+						Tags:   map[string]string{"t": "t1"},
+						Values: map[string]interface{}{"n": "42"},
+					},
+				},
+			},
+			want: []*formatters.EventMsg{
+				{
+					Tags:   map[string]string{"new_tag": "t1"},
+					Values: map[string]interface{}{"number": "42"},
+				},
+				{
+					Tags:   map[string]string{"t": "t1"},
+					Values: map[string]interface{}{"n": "42"},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			in := formatters.EventProcessors["event-combine"]
+			p := in()
+			err := p.Init(tt.fields.processorConfig, formatters.WithProcessors(tt.fields.processorsSet))
+			if err != nil {
+				t.Logf("%s failed to init the processor: %v", tt.name, err)
+				t.Fail()
+			}
+			if got := p.Apply(tt.args.es...); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("combine.Apply() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/formatters/event_convert/event_convert.go b/formatters/event_convert/event_convert.go
index c33c027a..c351b78f 100644
--- a/formatters/event_convert/event_convert.go
+++ b/formatters/event_convert/event_convert.go
@@ -29,8 +29,8 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-// Convert converts the value with key matching one of regexes, to the specified Type
-type Convert struct {
+// convert converts the value with key matching one of regexes, to the specified Type
+type convert struct {
 	Values []string `mapstructure:"value-names,omitempty" json:"value-names,omitempty"`
 	Type   string   `mapstructure:"type,omitempty" json:"type,omitempty"`
 	Debug  bool     `mapstructure:"debug,omitempty" json:"debug,omitempty"`
@@ -41,13 +41,13 @@ type Convert struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &Convert{
+		return &convert{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (c *Convert) Init(cfg interface{}, opts ...formatters.Option) error {
+func (c *convert) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, c)
 	if err != nil {
 		return err
@@ -74,7 +74,7 @@ func (c *Convert) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (c *Convert) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (c *convert) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	for _, e := range es {
 		if e == nil {
 			continue
@@ -125,7 +125,7 @@ func (c *Convert) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (c *Convert) WithLogger(l *log.Logger) {
+func (c *convert) WithLogger(l *log.Logger) {
 	if c.Debug && l != nil {
 		c.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if c.Debug {
@@ -133,9 +133,11 @@ func (c *Convert) WithLogger(l *log.Logger) {
 	}
 }
 
-func (c *Convert) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (c *convert) WithTargets(tcs map[string]*types.TargetConfig) {}
 
-func (c *Convert) WithActions(act map[string]map[string]interface{}) {}
+func (c *convert) WithActions(act map[string]map[string]interface{}) {}
+
+func (c *convert) WithProcessors(procs map[string]map[string]any) {}
 
 func convertToInt(i interface{}) (int, error) {
 	switch i := i.(type) {
diff --git a/formatters/event_data_convert/event_data_convert.go b/formatters/event_data_convert/event_data_convert.go
index 1beb438f..ca2ec016 100644
--- a/formatters/event_data_convert/event_data_convert.go
+++ b/formatters/event_data_convert/event_data_convert.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 
 	units "github.com/bcicen/go-units"
+
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
@@ -142,6 +143,8 @@ func (c *dataConvert) WithTargets(tcs map[string]*types.TargetConfig) {}
 
 func (c *dataConvert) WithActions(act map[string]map[string]interface{}) {}
 
+func (c *dataConvert) WithProcessors(procs map[string]map[string]any) {}
+
 func (c *dataConvert) convertData(k string, i interface{}, from *units.Unit) (float64, error) {
 	if from == nil && c.From == "" {
 		from = unitFromName(k)
diff --git a/formatters/event_date_string/event_date_string.go b/formatters/event_date_string/event_date_string.go
index ae106e63..6a1c1625 100644
--- a/formatters/event_date_string/event_date_string.go
+++ b/formatters/event_date_string/event_date_string.go
@@ -28,10 +28,10 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-// DateString converts Tags and/or Values of unix timestamp to a human readable format.
+// dateString converts Tags and/or Values of unix timestamp to a human readable format.
 // Precision specifies the unit of the received timestamp, s, ms, us or ns.
 // DateTimeFormat is the desired datetime format, it defaults to RFC3339
-type DateString struct {
+type dateString struct {
 	Tags      []string `mapstructure:"tag-names,omitempty" json:"tag-names,omitempty"`
 	Values    []string `mapstructure:"value-names,omitempty" json:"value-names,omitempty"`
 	Precision string   `mapstructure:"precision,omitempty" json:"precision,omitempty"`
@@ -47,13 +47,13 @@ type DateString struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &DateString{
+		return &dateString{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (d *DateString) Init(cfg interface{}, opts ...formatters.Option) error {
+func (d *dateString) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, d)
 	if err != nil {
 		return err
@@ -99,7 +99,7 @@ func (d *DateString) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (d *DateString) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (d *dateString) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	for _, e := range es {
 		if e == nil {
 			continue
@@ -163,7 +163,7 @@ func (d *DateString) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (d *DateString) WithLogger(l *log.Logger) {
+func (d *dateString) WithLogger(l *log.Logger) {
 	if d.Debug && l != nil {
 		d.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if d.Debug {
@@ -171,9 +171,11 @@ func (d *DateString) WithLogger(l *log.Logger) {
 	}
 }
 
-func (d *DateString) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (d *dateString) WithTargets(tcs map[string]*types.TargetConfig) {}
 
-func (d *DateString) WithActions(act map[string]map[string]interface{}) {}
+func (d *dateString) WithActions(act map[string]map[string]interface{}) {}
+
+func (d *dateString) WithProcessors(procs map[string]map[string]any) {}
 
 func convertToInt(i interface{}) (int, error) {
 	switch i := i.(type) {
diff --git a/formatters/event_delete/event_delete.go b/formatters/event_delete/event_delete.go
index fea9caeb..77021042 100644
--- a/formatters/event_delete/event_delete.go
+++ b/formatters/event_delete/event_delete.go
@@ -25,8 +25,8 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-// Delete, deletes ALL the tags or values matching one of the regexes
-type Delete struct {
+// deletep, deletes ALL the tags or values matching one of the regexes
+type deletep struct {
 	Tags       []string `mapstructure:"tags,omitempty" json:"tags,omitempty"`
 	Values     []string `mapstructure:"values,omitempty" json:"values,omitempty"`
 	TagNames   []string `mapstructure:"tag-names,omitempty" json:"tag-names,omitempty"`
@@ -44,13 +44,13 @@ type Delete struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &Delete{
+		return &deletep{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (d *Delete) Init(cfg interface{}, opts ...formatters.Option) error {
+func (d *deletep) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, d)
 	if err != nil {
 		return err
@@ -105,7 +105,7 @@ func (d *Delete) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (d *Delete) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (d *deletep) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	for _, e := range es {
 		if e == nil {
 			continue
@@ -144,7 +144,7 @@ func (d *Delete) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (d *Delete) WithLogger(l *log.Logger) {
+func (d *deletep) WithLogger(l *log.Logger) {
 	if d.Debug && l != nil {
 		d.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if d.Debug {
@@ -152,6 +152,8 @@ func (d *Delete) WithLogger(l *log.Logger) {
 	}
 }
 
-func (d *Delete) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (d *deletep) WithTargets(tcs map[string]*types.TargetConfig) {}
 
-func (d *Delete) WithActions(act map[string]map[string]interface{}) {}
+func (d *deletep) WithActions(act map[string]map[string]interface{}) {}
+
+func (d *deletep) WithProcessors(procs map[string]map[string]any) {}
diff --git a/formatters/event_drop/event_drop.go b/formatters/event_drop/event_drop.go
index 86697174..288d0099 100644
--- a/formatters/event_drop/event_drop.go
+++ b/formatters/event_drop/event_drop.go
@@ -17,6 +17,7 @@ import (
 	"strings"
 
 	"github.com/itchyny/gojq"
+
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
@@ -27,8 +28,8 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-// Drop Drops the msg if ANY of the Tags or Values regexes are matched
-type Drop struct {
+// drop Drops the msg if ANY of the Tags or Values regexes are matched
+type drop struct {
 	Condition  string   `mapstructure:"condition,omitempty"`
 	TagNames   []string `mapstructure:"tag-names,omitempty" json:"tag-names,omitempty"`
 	ValueNames []string `mapstructure:"value-names,omitempty" json:"value-names,omitempty"`
@@ -46,13 +47,13 @@ type Drop struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &Drop{
+		return &drop{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (d *Drop) Init(cfg interface{}, opts ...formatters.Option) error {
+func (d *drop) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, d)
 	if err != nil {
 		return err
@@ -115,7 +116,7 @@ func (d *Drop) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (d *Drop) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (d *drop) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	i := 0
 	for _, e := range es {
 		if !d.drop(e) {
@@ -130,7 +131,7 @@ func (d *Drop) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (d *Drop) WithLogger(l *log.Logger) {
+func (d *drop) WithLogger(l *log.Logger) {
 	if d.Debug && l != nil {
 		d.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if d.Debug {
@@ -138,11 +139,13 @@ func (d *Drop) WithLogger(l *log.Logger) {
 	}
 }
 
-func (d *Drop) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (d *drop) WithTargets(tcs map[string]*types.TargetConfig) {}
+
+func (d *drop) WithActions(act map[string]map[string]interface{}) {}
 
-func (d *Drop) WithActions(act map[string]map[string]interface{}) {}
+func (d *drop) WithProcessors(procs map[string]map[string]any) {}
 
-func (d *Drop) drop(e *formatters.EventMsg) bool {
+func (d *drop) drop(e *formatters.EventMsg) bool {
 	if d.Condition != "" {
 		ok, err := formatters.CheckCondition(d.code, e)
 		if err != nil {
diff --git a/formatters/event_duration_convert/event_duration_convert.go b/formatters/event_duration_convert/event_duration_convert.go
index cbc183d5..15f43c3c 100644
--- a/formatters/event_duration_convert/event_duration_convert.go
+++ b/formatters/event_duration_convert/event_duration_convert.go
@@ -121,6 +121,8 @@ func (c *durationConvert) WithTargets(tcs map[string]*types.TargetConfig) {}
 
 func (c *durationConvert) WithActions(act map[string]map[string]interface{}) {}
 
+func (c *durationConvert) WithProcessors(procs map[string]map[string]any) {}
+
 func (c *durationConvert) convertDuration(k string, i interface{}) (int64, error) {
 	switch i := i.(type) {
 	case string:
diff --git a/formatters/event_extract_tags/event_extract_tags.go b/formatters/event_extract_tags/event_extract_tags.go
index 9b3b926d..aabe3e5d 100644
--- a/formatters/event_extract_tags/event_extract_tags.go
+++ b/formatters/event_extract_tags/event_extract_tags.go
@@ -144,6 +144,8 @@ func (p *extractTags) WithTargets(tcs map[string]*types.TargetConfig) {}
 
 func (p *extractTags) WithActions(act map[string]map[string]interface{}) {}
 
+func (p *extractTags) WithProcessors(procs map[string]map[string]any) {}
+
 func (p *extractTags) addTags(e *formatters.EventMsg, re *regexp.Regexp, s string) {
 	if e.Tags == nil {
 		e.Tags = make(map[string]string)
diff --git a/formatters/event_group_by/event_group_by.go b/formatters/event_group_by/event_group_by.go
index 87801881..4eb75bbb 100644
--- a/formatters/event_group_by/event_group_by.go
+++ b/formatters/event_group_by/event_group_by.go
@@ -107,6 +107,8 @@ func (p *groupBy) WithTargets(tcs map[string]*types.TargetConfig) {}
 
 func (p *groupBy) WithActions(act map[string]map[string]interface{}) {}
 
+func (p *groupBy) WithProcessors(procs map[string]map[string]any) {}
+
 func (p *groupBy) byTags(es []*formatters.EventMsg) []*formatters.EventMsg {
 	if len(p.Tags) == 0 {
 		return es
diff --git a/formatters/event_jq/event_jq.go b/formatters/event_jq/event_jq.go
index 310cd306..a84e971b 100644
--- a/formatters/event_jq/event_jq.go
+++ b/formatters/event_jq/event_jq.go
@@ -16,6 +16,7 @@ import (
 	"strings"
 
 	"github.com/itchyny/gojq"
+
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
@@ -201,3 +202,5 @@ func (p *jq) WithLogger(l *log.Logger) {
 func (p *jq) WithTargets(tcs map[string]*types.TargetConfig) {}
 
 func (p *jq) WithActions(act map[string]map[string]interface{}) {}
+
+func (p *jq) WithProcessors(procs map[string]map[string]any) {}
diff --git a/formatters/event_merge/event_merge.go b/formatters/event_merge/event_merge.go
index a9de61f2..559e056c 100644
--- a/formatters/event_merge/event_merge.go
+++ b/formatters/event_merge/event_merge.go
@@ -24,8 +24,8 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-// Merge merges a list of event messages into one or multiple messages based on some criteria
-type Merge struct {
+// merge merges a list of event messages into one or multiple messages based on some criteria
+type merge struct {
 	Always bool `mapstructure:"always,omitempty" json:"always,omitempty"`
 	Debug  bool `mapstructure:"debug,omitempty" json:"debug,omitempty"`
 
@@ -34,13 +34,13 @@ type Merge struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &Merge{
+		return &merge{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (p *Merge) Init(cfg interface{}, opts ...formatters.Option) error {
+func (p *merge) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, p)
 	if err != nil {
 		return err
@@ -60,7 +60,7 @@ func (p *Merge) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (p *Merge) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (p *merge) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	if len(es) == 0 {
 		return nil
 	}
@@ -70,7 +70,7 @@ func (p *Merge) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 				continue
 			}
 			if i > 0 {
-				merge(es[0], e)
+				mergeEvents(es[0], e)
 			}
 		}
 		return []*formatters.EventMsg{es[0]}
@@ -82,7 +82,7 @@ func (p *Merge) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 			continue
 		}
 		if idx, ok := timestamps[e.Timestamp]; ok {
-			merge(result[idx], e)
+			mergeEvents(result[idx], e)
 			continue
 		}
 		result = append(result, e)
@@ -91,7 +91,7 @@ func (p *Merge) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return result
 }
 
-func (p *Merge) WithLogger(l *log.Logger) {
+func (p *merge) WithLogger(l *log.Logger) {
 	if p.Debug && l != nil {
 		p.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if p.Debug {
@@ -99,11 +99,13 @@ func (p *Merge) WithLogger(l *log.Logger) {
 	}
 }
 
-func (p *Merge) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (p *merge) WithTargets(tcs map[string]*types.TargetConfig) {}
 
-func (p *Merge) WithActions(act map[string]map[string]interface{}) {}
+func (p *merge) WithActions(act map[string]map[string]interface{}) {}
 
-func merge(e1, e2 *formatters.EventMsg) {
+func (p *merge) WithProcessors(procs map[string]map[string]any) {}
+
+func mergeEvents(e1, e2 *formatters.EventMsg) {
 	if e1.Tags == nil {
 		e1.Tags = make(map[string]string)
 	}
diff --git a/formatters/event_override_ts/event_override_ts.go b/formatters/event_override_ts/event_override_ts.go
index f79c0780..a2152be8 100644
--- a/formatters/event_override_ts/event_override_ts.go
+++ b/formatters/event_override_ts/event_override_ts.go
@@ -25,8 +25,8 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-// OverrideTS Overrides the message timestamp with the local time
-type OverrideTS struct {
+// overrideTS Overrides the message timestamp with the local time
+type overrideTS struct {
 	//formatters.EventProcessor
 
 	Precision string `mapstructure:"precision,omitempty" json:"precision,omitempty"`
@@ -37,13 +37,13 @@ type OverrideTS struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &OverrideTS{
+		return &overrideTS{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (o *OverrideTS) Init(cfg interface{}, opts ...formatters.Option) error {
+func (o *overrideTS) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, o)
 	if err != nil {
 		return err
@@ -65,7 +65,7 @@ func (o *OverrideTS) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (o *OverrideTS) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (o *overrideTS) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	for _, e := range es {
 		if e == nil {
 			continue
@@ -86,7 +86,7 @@ func (o *OverrideTS) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (o *OverrideTS) WithLogger(l *log.Logger) {
+func (o *overrideTS) WithLogger(l *log.Logger) {
 	if o.Debug && l != nil {
 		o.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if o.Debug {
@@ -94,6 +94,8 @@ func (o *OverrideTS) WithLogger(l *log.Logger) {
 	}
 }
 
-func (o *OverrideTS) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (o *overrideTS) WithTargets(tcs map[string]*types.TargetConfig) {}
 
-func (o *OverrideTS) WithActions(act map[string]map[string]interface{}) {}
+func (o *overrideTS) WithActions(act map[string]map[string]interface{}) {}
+
+func (o *overrideTS) WithProcessors(procs map[string]map[string]any) {}
diff --git a/formatters/event_rate_limit/event_rate_limit.go b/formatters/event_rate_limit/event_rate_limit.go
new file mode 100644
index 00000000..02501c5d
--- /dev/null
+++ b/formatters/event_rate_limit/event_rate_limit.go
@@ -0,0 +1,145 @@
+package event_rate_limit
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"sort"
+	"time"
+
+	lru "github.com/hashicorp/golang-lru/v2"
+
+	"github.com/openconfig/gnmic/formatters"
+	"github.com/openconfig/gnmic/types"
+	"github.com/openconfig/gnmic/utils"
+)
+
+const (
+	processorType          = "event-rate-limit"
+	loggingPrefix          = "[" + processorType + "] "
+	defaultCacheSize       = 1000
+	oneSecond        int64 = int64(time.Second)
+)
+
+var (
+	eqChar = []byte("=")
+	lfChar = []byte("\n")
+)
+
+// rateLimit rate-limits the message to the given rate.
+type rateLimit struct {
+	// formatters.EventProcessor
+
+	PerSecondLimit float64 `mapstructure:"per-second,omitempty" json:"per-second,omitempty"`
+	CacheSize      int     `mapstructure:"cache-size,omitempty" json:"cache-size,omitempty"`
+	Debug          bool    `mapstructure:"debug,omitempty" json:"debug,omitempty"`
+
+	// eventIndex is an lru cache used to compare the events hash with known value.
+	// LRU cache seems like a good choice because we expect the rate-limiter to be
+	// most useful in burst scenarios.
+	// We need some form of control over the size of the cache to contain RAM usage
+	// so LRU is good in that respect also.
+	eventIndex *lru.Cache[string, int64]
+	logger     *log.Logger
+}
+
+func init() {
+	formatters.Register(processorType, func() formatters.EventProcessor {
+		return &rateLimit{
+			logger: log.New(io.Discard, "", 0),
+		}
+	})
+}
+
+func (o *rateLimit) Init(cfg interface{}, opts ...formatters.Option) error {
+	err := formatters.DecodeConfig(cfg, o)
+	if err != nil {
+		return err
+	}
+	for _, opt := range opts {
+		opt(o)
+	}
+	if o.CacheSize <= 0 {
+		o.logger.Printf("using default value for lru size %d", defaultCacheSize)
+		o.CacheSize = defaultCacheSize
+
+	}
+	if o.PerSecondLimit <= 0 {
+		return fmt.Errorf("provided limit is %f, must be greater than 0", o.PerSecondLimit)
+	}
+	if o.logger.Writer() != io.Discard {
+		b, err := json.Marshal(o)
+		if err != nil {
+			o.logger.Printf("initialized processor '%s': %+v", processorType, o)
+			return nil
+		}
+		o.logger.Printf("initialized processor '%s': %s", processorType, string(b))
+	}
+
+	o.eventIndex, err = lru.New[string, int64](o.CacheSize)
+	if err != nil {
+		return fmt.Errorf("failed to initialize cache: %w", err)
+	}
+	return nil
+}
+
+func (o *rateLimit) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+	validEs := make([]*formatters.EventMsg, 0, len(es))
+
+	for _, e := range es {
+		if e == nil {
+			continue
+		}
+		h := hashEvent(e)
+		ts, has := o.eventIndex.Get(h)
+		// we check that we have the event hash in the map, if not, it's the first time we see the event
+		if val := float64(e.Timestamp-ts) * o.PerSecondLimit; has && e.Timestamp != ts && int64(val) < oneSecond {
+			// reject event
+			o.logger.Printf("dropping event val %.2f lower than configured rate", val)
+			continue
+		}
+		// retain the last event that passed through
+		o.eventIndex.Add(h, e.Timestamp)
+		validEs = append(validEs, e)
+	}
+
+	return validEs
+}
+
+func hashEvent(e *formatters.EventMsg) string {
+	h := sha256.New()
+	tagKeys := make([]string, len(e.Tags))
+	i := 0
+	for tagKey := range e.Tags {
+		tagKeys[i] = tagKey
+		i++
+	}
+	sort.Strings(tagKeys)
+
+	for _, tagKey := range tagKeys {
+		h.Write([]byte(tagKey))
+		h.Write(eqChar)
+		h.Write([]byte(e.Tags[tagKey]))
+		h.Write(lfChar)
+	}
+
+	return hex.EncodeToString(h.Sum(nil))
+}
+
+func (o *rateLimit) WithLogger(l *log.Logger) {
+	if o.Debug && l != nil {
+		o.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
+	} else if o.Debug {
+		o.logger = log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags)
+	}
+}
+
+func (o *rateLimit) WithTargets(tcs map[string]*types.TargetConfig) {}
+
+func (o *rateLimit) WithActions(act map[string]map[string]interface{}) {}
+
+func (o *rateLimit) WithProcessors(procs map[string]map[string]any) {}
diff --git a/formatters/event_rate_limit/event_rate_limit_test.go b/formatters/event_rate_limit/event_rate_limit_test.go
new file mode 100644
index 00000000..714198e8
--- /dev/null
+++ b/formatters/event_rate_limit/event_rate_limit_test.go
@@ -0,0 +1,363 @@
+package event_rate_limit
+
+import (
+	"testing"
+
+	"github.com/openconfig/gnmic/formatters"
+)
+
+type item struct {
+	input  []*formatters.EventMsg
+	output []*formatters.EventMsg
+}
+
+var testset = map[string]struct {
+	processor map[string]interface{}
+	tests     []item
+}{
+	"1pps-notags-pass": {
+		processor: map[string]interface{}{
+			"type":  processorType,
+			"debug": true,
+			"per-second": 1.0,
+		},
+		tests: []item{
+			{
+				input:  nil,
+				output: nil,
+			},
+			{
+				input: []*formatters.EventMsg{},
+			},
+			{
+				input: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+					},
+					{
+						Timestamp: 1e9+1,
+					},
+				},
+				output: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+					},
+					{
+						Timestamp: 1e9+1,
+					},
+				},
+			},
+		},
+	},
+	"1pps-tags-pass": {
+		processor: map[string]interface{}{
+			"type":      processorType,
+			"per-second": 1.0,
+			"debug":     true,
+		},
+		tests: []item{
+			{
+				input:  nil,
+				output: nil,
+			},
+			{
+				input: []*formatters.EventMsg{},
+			},
+			{
+				input: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+					{
+						Timestamp: 1,
+					},
+					{
+						Timestamp: 1+1e9,
+					},
+					{
+						Timestamp: 1e9+1,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+				},
+				output: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+					{
+						Timestamp: 1,
+					},
+					{
+						Timestamp: 1+1e9,
+					},
+					{
+						Timestamp: 1e9+1,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+				},
+			},
+		},
+	},
+	"1pps-notags-drop": {
+		processor: map[string]interface{}{
+			"type":  processorType,
+			"debug": true,
+			"per-second": 1.0,
+		},
+		tests: []item{
+			{
+				input:  nil,
+				output: nil,
+			},
+			{
+				input: []*formatters.EventMsg{},
+			},
+			{
+				input: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+					},
+					{
+						Timestamp: 1e9-1,
+					},
+				},
+				output: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+					},
+				},
+			},
+		},
+	},
+	"1pps-tags-drop": {
+		processor: map[string]interface{}{
+			"type":      processorType,
+			"per-second": 1.0,
+			"debug":     true,
+		},
+		tests: []item{
+			{
+				input:  nil,
+				output: nil,
+			},
+			{
+				input: []*formatters.EventMsg{},
+			},
+			{
+				input: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+					{
+						Timestamp: 1,
+					},
+					{
+						Timestamp: 1e9-1,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+				},
+				output: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+					{
+						Timestamp: 1,
+					},
+				},
+			},
+		},
+	},
+	"100pps-tags-pass": {
+		processor: map[string]interface{}{
+			"type":      processorType,
+			"per-second": 100.0,
+			"debug":     true,
+		},
+		tests: []item{
+			{
+				input:  nil,
+				output: nil,
+			},
+			{
+				input: []*formatters.EventMsg{},
+			},
+			{
+				input: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+					{
+						Timestamp: 1,
+					},
+					{
+						Timestamp: 1e9/100,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+				},
+				output: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+					{
+						Timestamp: 1,
+					},
+					{
+						Timestamp: 1e9/100,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+				},
+			},
+		},
+	},
+	"100pps-tags-drop": {
+		processor: map[string]interface{}{
+			"type":      processorType,
+			"per-second": 100.0,
+			"debug":     true,
+		},
+		tests: []item{
+			{
+				input:  nil,
+				output: nil,
+			},
+			{
+				input: []*formatters.EventMsg{},
+			},
+			{
+				input: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+					{
+						Timestamp: 1,
+					},
+					{
+						Timestamp: 1e9/100-1,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+				},
+				output: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+						Tags: map[string]string{
+							"a": "val-x",
+							"b": "val-y",
+						},
+					},
+					{
+						Timestamp: 1,
+					},
+				},
+			},
+		},
+	},
+	"same-ts-pass": {
+		processor: map[string]interface{}{
+			"type":      processorType,
+			"per-second": 100.0,
+			"debug":     true,
+		},
+		tests: []item{
+			{
+				input:  nil,
+				output: nil,
+			},
+			{
+				input: []*formatters.EventMsg{},
+			},
+			{
+				input: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+					},
+					{
+						Timestamp: 0,
+					},
+				},
+				output: []*formatters.EventMsg{
+					{
+						Timestamp: 0,
+					},
+					{
+						Timestamp: 0,
+					},
+				},
+			},
+		},
+	},
+}
+
+func TestRateLimit(t *testing.T) {
+	for name, ts := range testset {
+		t.Log(name)
+		if typ, ok := ts.processor["type"]; ok {
+			t.Log("found type")
+			if pi, ok := formatters.EventProcessors[typ.(string)]; ok {
+				t.Log("found processor")
+				p := pi()
+				err := p.Init(ts.processor, formatters.WithLogger(nil))
+				if err != nil {
+					t.Errorf("failed to initialize processors: %v", err)
+					return
+				}
+				t.Logf("initialized for test %s: %+v", name, p)
+				for i, item := range ts.tests {
+					t.Run(name, func(t *testing.T) {
+						t.Logf("running test item %d", i)
+						outs := p.Apply(item.input...)
+						if len(outs) != len(item.output) {
+							t.Logf("failed at event rate_limit, item %d", i)
+							t.Logf("different number of events between output=%d and wanted=%d", len(outs), len(item.output))
+							t.Fail()
+						}
+					})
+				}
+			}
+		}
+	}
+}
diff --git a/formatters/event_starlark/event_starlark.go b/formatters/event_starlark/event_starlark.go
index c0c35c2c..18614b46 100644
--- a/formatters/event_starlark/event_starlark.go
+++ b/formatters/event_starlark/event_starlark.go
@@ -17,13 +17,14 @@ import (
 	"os"
 	"sync"
 
-	"github.com/openconfig/gnmic/formatters"
-	"github.com/openconfig/gnmic/types"
-	"github.com/openconfig/gnmic/utils"
 	"go.starlark.net/lib/math"
 	"go.starlark.net/lib/time"
 	"go.starlark.net/resolve"
 	"go.starlark.net/starlark"
+
+	"github.com/openconfig/gnmic/formatters"
+	"github.com/openconfig/gnmic/types"
+	"github.com/openconfig/gnmic/utils"
 )
 
 const (
@@ -149,7 +150,11 @@ func (p *starlarkProc) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg
 	}
 	r, err := starlark.Call(p.thread, p.applyFn, sevs, nil)
 	if err != nil {
-		p.logger.Printf("failed to run script: %v", err)
+		if p.Debug {
+			p.logger.Printf("failed to run script with input %v: %v", sevs, err)
+		} else {
+			p.logger.Printf("failed to run script: %v", err)
+		}
 		return es
 	}
 	if p.Debug {
@@ -195,6 +200,8 @@ func (p *starlarkProc) WithTargets(tcs map[string]*types.TargetConfig) {}
 
 func (p *starlarkProc) WithActions(act map[string]map[string]interface{}) {}
 
+func (p *starlarkProc) WithProcessors(procs map[string]map[string]any) {}
+
 func (p *starlarkProc) sourceProgram(builtins starlark.StringDict) (*starlark.Program, error) {
 	var src interface{}
 	if p.Source != "" {
diff --git a/formatters/event_strings/event_strings.go b/formatters/event_strings/event_strings.go
index f2277ce8..db02b429 100644
--- a/formatters/event_strings/event_strings.go
+++ b/formatters/event_strings/event_strings.go
@@ -17,11 +17,12 @@ import (
 	"regexp"
 	"strings"
 
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
+
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
-	"golang.org/x/text/cases"
-	"golang.org/x/text/language"
 )
 
 const (
@@ -31,8 +32,8 @@ const (
 	valueField    = "value"
 )
 
-// Strings provides some of Golang's strings functions to transform: tags, tag names, values and value names
-type Strings struct {
+// stringsp provides some of Golang's strings functions to transform: tags, tag names, values and value names
+type stringsp struct {
 	Tags       []string                `mapstructure:"tags,omitempty" json:"tags,omitempty"`
 	Values     []string                `mapstructure:"values,omitempty" json:"values,omitempty"`
 	TagNames   []string                `mapstructure:"tag-names,omitempty" json:"tag-names,omitempty"`
@@ -77,13 +78,13 @@ type transform struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &Strings{
+		return &stringsp{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (s *Strings) Init(cfg interface{}, opts ...formatters.Option) error {
+func (s *stringsp) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, s)
 	if err != nil {
 		return err
@@ -150,7 +151,7 @@ func (s *Strings) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (s *Strings) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (s *stringsp) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	for _, e := range es {
 		if e == nil {
 			continue
@@ -189,7 +190,7 @@ func (s *Strings) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (s *Strings) WithLogger(l *log.Logger) {
+func (s *stringsp) WithLogger(l *log.Logger) {
 	if s.Debug && l != nil {
 		s.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if s.Debug {
@@ -197,11 +198,13 @@ func (s *Strings) WithLogger(l *log.Logger) {
 	}
 }
 
-func (s *Strings) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (s *stringsp) WithTargets(tcs map[string]*types.TargetConfig) {}
+
+func (s *stringsp) WithActions(act map[string]map[string]interface{}) {}
 
-func (s *Strings) WithActions(act map[string]map[string]interface{}) {}
+func (s *stringsp) WithProcessors(procs map[string]map[string]any) {}
 
-func (s *Strings) applyValueTransformations(e *formatters.EventMsg, k string, v interface{}) {
+func (s *stringsp) applyValueTransformations(e *formatters.EventMsg, k string, v interface{}) {
 	for _, trans := range s.Transforms {
 		for _, t := range trans {
 			if !t.Keep {
@@ -213,7 +216,7 @@ func (s *Strings) applyValueTransformations(e *formatters.EventMsg, k string, v
 	}
 }
 
-func (s *Strings) applyTagTransformations(e *formatters.EventMsg, k, v string) {
+func (s *stringsp) applyTagTransformations(e *formatters.EventMsg, k, v string) {
 	for _, trans := range s.Transforms {
 		for _, t := range trans {
 			if !t.Keep {
diff --git a/formatters/event_to_tag/event_to_tag.go b/formatters/event_to_tag/event_to_tag.go
index ab5e389c..7bd86695 100644
--- a/formatters/event_to_tag/event_to_tag.go
+++ b/formatters/event_to_tag/event_to_tag.go
@@ -25,9 +25,9 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-// ToTag moves ALL values matching any of the regex in .Values to the EventMsg.Tags map.
+// toTag moves ALL values matching any of the regex in .Values to the EventMsg.Tags map.
 // if .Keep is true, the matching values are not deleted from EventMsg.Tags
-type ToTag struct {
+type toTag struct {
 	Values     []string `mapstructure:"values,omitempty" json:"values,omitempty"`
 	ValueNames []string `mapstructure:"value-names,omitempty" json:"value-names,omitempty"`
 	Keep       bool     `mapstructure:"keep,omitempty" json:"keep,omitempty"`
@@ -41,13 +41,13 @@ type ToTag struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &ToTag{
+		return &toTag{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (t *ToTag) Init(cfg interface{}, opts ...formatters.Option) error {
+func (t *toTag) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, t)
 	if err != nil {
 		return err
@@ -82,7 +82,7 @@ func (t *ToTag) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (t *ToTag) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (t *toTag) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	for _, e := range es {
 		if e == nil {
 			continue
@@ -117,7 +117,7 @@ func (t *ToTag) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (t *ToTag) WithLogger(l *log.Logger) {
+func (t *toTag) WithLogger(l *log.Logger) {
 	if t.Debug && l != nil {
 		t.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if t.Debug {
@@ -125,6 +125,8 @@ func (t *ToTag) WithLogger(l *log.Logger) {
 	}
 }
 
-func (t *ToTag) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (t *toTag) WithTargets(tcs map[string]*types.TargetConfig) {}
 
-func (t *ToTag) WithActions(act map[string]map[string]interface{}) {}
+func (t *toTag) WithActions(act map[string]map[string]interface{}) {}
+
+func (t *toTag) WithProcessors(procs map[string]map[string]any) {}
diff --git a/formatters/event_trigger/event_trigger.go b/formatters/event_trigger/event_trigger.go
index 86f51ecd..4e6dd9e6 100644
--- a/formatters/event_trigger/event_trigger.go
+++ b/formatters/event_trigger/event_trigger.go
@@ -20,12 +20,13 @@ import (
 	"time"
 
 	"github.com/itchyny/gojq"
+	"gopkg.in/yaml.v2"
+
 	"github.com/openconfig/gnmic/actions"
 	_ "github.com/openconfig/gnmic/actions/all"
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
-	"gopkg.in/yaml.v2"
 )
 
 const (
@@ -34,8 +35,8 @@ const (
 	defaultCondition = "any([true])"
 )
 
-// Trigger triggers an action when certain conditions are met
-type Trigger struct {
+// trigger triggers an action when certain conditions are met
+type trigger struct {
 	Condition      string                 `mapstructure:"condition,omitempty"`
 	MinOccurrences int                    `mapstructure:"min-occurrences,omitempty"`
 	MaxOccurrences int                    `mapstructure:"max-occurrences,omitempty"`
@@ -59,13 +60,13 @@ type Trigger struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &Trigger{
+		return &trigger{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (p *Trigger) Init(cfg interface{}, opts ...formatters.Option) error {
+func (p *trigger) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, p)
 	if err != nil {
 		return err
@@ -109,7 +110,7 @@ func (p *Trigger) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (p *Trigger) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (p *trigger) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	now := time.Now()
 	for _, e := range es {
 		if e == nil {
@@ -136,7 +137,7 @@ func (p *Trigger) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 	return es
 }
 
-func (p *Trigger) WithLogger(l *log.Logger) {
+func (p *trigger) WithLogger(l *log.Logger) {
 	if p.Debug && l != nil {
 		p.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if p.Debug {
@@ -144,21 +145,21 @@ func (p *Trigger) WithLogger(l *log.Logger) {
 	}
 }
 
-func (p *Trigger) WithTargets(tcs map[string]*types.TargetConfig) {
+func (p *trigger) WithTargets(tcs map[string]*types.TargetConfig) {
 	if p.Debug {
 		p.logger.Printf("with targets: %+v", tcs)
 	}
 	p.targets = tcs
 }
 
-func (p *Trigger) WithActions(acts map[string]map[string]interface{}) {
+func (p *trigger) WithActions(acts map[string]map[string]interface{}) {
 	if p.Debug {
 		p.logger.Printf("with actions: %+v", acts)
 	}
 	p.acts = acts
 }
 
-func (p *Trigger) initializeAction(cfg map[string]interface{}) error {
+func (p *trigger) initializeAction(cfg map[string]interface{}) error {
 	if len(cfg) == 0 {
 		return errors.New("missing action definition")
 	}
@@ -182,7 +183,7 @@ func (p *Trigger) initializeAction(cfg map[string]interface{}) error {
 	return errors.New("missing type field under action")
 }
 
-func (p *Trigger) String() string {
+func (p *trigger) String() string {
 	b, err := json.Marshal(p)
 	if err != nil {
 		return ""
@@ -190,7 +191,7 @@ func (p *Trigger) String() string {
 	return string(b)
 }
 
-func (p *Trigger) setDefaults() error {
+func (p *trigger) setDefaults() error {
 	if p.Condition == "" {
 		p.Condition = defaultCondition
 	}
@@ -209,7 +210,7 @@ func (p *Trigger) setDefaults() error {
 	return nil
 }
 
-func (p *Trigger) readVars() error {
+func (p *trigger) readVars() error {
 	if p.VarsFile == "" {
 		p.vars = p.Vars
 		return nil
@@ -227,7 +228,7 @@ func (p *Trigger) readVars() error {
 	return nil
 }
 
-func (p *Trigger) triggerActions(e *formatters.EventMsg) {
+func (p *trigger) triggerActions(e *formatters.EventMsg) {
 	actx := &actions.Context{Input: e, Env: make(map[string]interface{}), Vars: p.vars}
 	for _, act := range p.actions {
 		res, err := act.Run(context.TODO(), actx)
@@ -240,7 +241,7 @@ func (p *Trigger) triggerActions(e *formatters.EventMsg) {
 	}
 }
 
-func (p *Trigger) evalOccurrencesWithinWindow(now time.Time) bool {
+func (p *trigger) evalOccurrencesWithinWindow(now time.Time) bool {
 	if p.occurrencesTimes == nil {
 		p.occurrencesTimes = make([]time.Time, 0)
 	}
@@ -278,3 +279,5 @@ func (p *Trigger) evalOccurrencesWithinWindow(now time.Time) bool {
 	}
 	return false
 }
+
+func (p *trigger) WithProcessors(procs map[string]map[string]any) {}
diff --git a/formatters/event_trigger/event_trigger_test.go b/formatters/event_trigger/event_trigger_test.go
index 80a1f6d5..67912f2b 100644
--- a/formatters/event_trigger/event_trigger_test.go
+++ b/formatters/event_trigger/event_trigger_test.go
@@ -172,12 +172,12 @@ var testset = map[string]struct {
 }
 
 var triggerOccWindowTestSet = map[string]struct {
-	t   *Trigger
+	t   *trigger
 	now time.Time
 	out bool
 }{
 	"defaults_0_occurrences": {
-		t: &Trigger{
+		t: &trigger{
 			logger:           log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:            true,
 			MinOccurrences:   1,
@@ -189,7 +189,7 @@ var triggerOccWindowTestSet = map[string]struct {
 		now: time.Now(),
 	},
 	"defaults_with_1_occurrence_in_window": {
-		t: &Trigger{
+		t: &trigger{
 			logger:         log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:          true,
 			MinOccurrences: 1,
@@ -204,7 +204,7 @@ var triggerOccWindowTestSet = map[string]struct {
 		now: time.Now(),
 	},
 	"defaults_with_1_occurrence_out_of_window": {
-		t: &Trigger{
+		t: &trigger{
 			logger:         log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:          true,
 			MinOccurrences: 1,
@@ -218,7 +218,7 @@ var triggerOccWindowTestSet = map[string]struct {
 		now: time.Now(),
 	},
 	"2max_1min_without_occurrences": {
-		t: &Trigger{
+		t: &trigger{
 			logger:           log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:            true,
 			MinOccurrences:   1,
@@ -230,7 +230,7 @@ var triggerOccWindowTestSet = map[string]struct {
 		now: time.Now(),
 	},
 	"2max_1min_with_1occurrence_in_window": {
-		t: &Trigger{
+		t: &trigger{
 			logger:         log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:          true,
 			MinOccurrences: 1,
@@ -244,7 +244,7 @@ var triggerOccWindowTestSet = map[string]struct {
 		now: time.Now(),
 	},
 	"2max_1min_with_2occurrences_in_window": {
-		t: &Trigger{
+		t: &trigger{
 			logger:         log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:          true,
 			MinOccurrences: 1,
@@ -260,7 +260,7 @@ var triggerOccWindowTestSet = map[string]struct {
 		now: time.Now(),
 	},
 	"2max_2min_without_occurrences": {
-		t: &Trigger{
+		t: &trigger{
 			logger:           log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:            true,
 			MinOccurrences:   2,
@@ -272,7 +272,7 @@ var triggerOccWindowTestSet = map[string]struct {
 		now: time.Now(),
 	},
 	"2max_2min_with_1occurrence_in_window": {
-		t: &Trigger{
+		t: &trigger{
 			logger:         log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:          true,
 			MinOccurrences: 2,
@@ -286,7 +286,7 @@ var triggerOccWindowTestSet = map[string]struct {
 		now: time.Now(),
 	},
 	"2max_2min_with_2occurrences_in_window": {
-		t: &Trigger{
+		t: &trigger{
 			logger:         log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:          true,
 			MinOccurrences: 2,
@@ -302,7 +302,7 @@ var triggerOccWindowTestSet = map[string]struct {
 		now: time.Now(),
 	},
 	"2max_2min_with_2occurrences_in_window_lastTrigger_out_of_window": {
-		t: &Trigger{
+		t: &trigger{
 			logger:         log.New(os.Stderr, loggingPrefix, utils.DefaultLoggingFlags),
 			Debug:          true,
 			MinOccurrences: 2,
diff --git a/formatters/event_value_tag/event_value_tag.go b/formatters/event_value_tag/event_value_tag.go
index c5f86cfc..7614dad2 100644
--- a/formatters/event_value_tag/event_value_tag.go
+++ b/formatters/event_value_tag/event_value_tag.go
@@ -25,7 +25,7 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-type ValueTag struct {
+type valueTag struct {
 	TagName   string `mapstructure:"tag-name,omitempty" json:"tag-name,omitempty"`
 	ValueName string `mapstructure:"value-name,omitempty" json:"value-name,omitempty"`
 	Consume   bool   `mapstructure:"consume,omitempty" json:"consume,omitempty"`
@@ -35,11 +35,11 @@ type ValueTag struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &ValueTag{logger: log.New(io.Discard, "", 0)}
+		return &valueTag{logger: log.New(io.Discard, "", 0)}
 	})
 }
 
-func (vt *ValueTag) Init(cfg interface{}, opts ...formatters.Option) error {
+func (vt *valueTag) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, vt)
 	if err != nil {
 		return err
@@ -64,7 +64,7 @@ type foo struct {
 	value interface{}
 }
 
-func (vt *ValueTag) Apply(evs ...*formatters.EventMsg) []*formatters.EventMsg {
+func (vt *valueTag) Apply(evs ...*formatters.EventMsg) []*formatters.EventMsg {
 	if vt.TagName == "" {
 		vt.TagName = vt.ValueName
 	}
@@ -92,7 +92,7 @@ func (vt *ValueTag) Apply(evs ...*formatters.EventMsg) []*formatters.EventMsg {
 	return evs
 }
 
-func (vt *ValueTag) WithLogger(l *log.Logger) {
+func (vt *valueTag) WithLogger(l *log.Logger) {
 	if vt.Debug && l != nil {
 		vt.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if vt.Debug {
@@ -100,9 +100,9 @@ func (vt *ValueTag) WithLogger(l *log.Logger) {
 	}
 }
 
-func (vt *ValueTag) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (vt *valueTag) WithTargets(tcs map[string]*types.TargetConfig) {}
 
-func (vt *ValueTag) WithActions(act map[string]map[string]interface{}) {}
+func (vt *valueTag) WithActions(act map[string]map[string]interface{}) {}
 
 func checkKeys(a map[string]string, b map[string]string) bool {
 	for k, v := range a {
@@ -116,3 +116,5 @@ func checkKeys(a map[string]string, b map[string]string) bool {
 	}
 	return true
 }
+
+func (vt *valueTag) WithProcessors(procs map[string]map[string]any) {}
diff --git a/formatters/event_write/event_write.go b/formatters/event_write/event_write.go
index f872e62e..d1899616 100644
--- a/formatters/event_write/event_write.go
+++ b/formatters/event_write/event_write.go
@@ -17,6 +17,7 @@ import (
 	"strings"
 
 	"github.com/itchyny/gojq"
+
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
@@ -27,7 +28,7 @@ const (
 	loggingPrefix = "[" + processorType + "] "
 )
 
-type Write struct {
+type write struct {
 	Condition  string   `mapstructure:"condition,omitempty"`
 	Tags       []string `mapstructure:"tags,omitempty" json:"tags,omitempty"`
 	Values     []string `mapstructure:"values,omitempty" json:"values,omitempty"`
@@ -50,13 +51,13 @@ type Write struct {
 
 func init() {
 	formatters.Register(processorType, func() formatters.EventProcessor {
-		return &Write{
+		return &write{
 			logger: log.New(io.Discard, "", 0),
 		}
 	})
 }
 
-func (p *Write) Init(cfg interface{}, opts ...formatters.Option) error {
+func (p *write) Init(cfg interface{}, opts ...formatters.Option) error {
 	err := formatters.DecodeConfig(cfg, p)
 	if err != nil {
 		return err
@@ -134,24 +135,23 @@ func (p *Write) Init(cfg interface{}, opts ...formatters.Option) error {
 	return nil
 }
 
-func (p *Write) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
+func (p *write) Apply(es ...*formatters.EventMsg) []*formatters.EventMsg {
 OUTER:
 	for _, e := range es {
 		if e == nil {
 			p.dst.Write([]byte(""))
 			continue
 		}
-		if p.code != nil {
-			ok, err := formatters.CheckCondition(p.code, e)
+
+		ok, err := formatters.CheckCondition(p.code, e)
+		if err != nil {
+			p.logger.Printf("condition check failed: %v", err)
+		}
+		if ok {
+			err := p.write(e)
 			if err != nil {
-				p.logger.Printf("condition check failed: %v", err)
-			}
-			if ok {
-				err := p.write(e)
-				if err != nil {
-					p.logger.Printf("failed to write to destination: %v", err)
-					continue OUTER
-				}
+				p.logger.Printf("failed to write to destination: %v", err)
+				continue OUTER
 			}
 		}
 		for k, v := range e.Values {
@@ -204,7 +204,7 @@ OUTER:
 	return es
 }
 
-func (p *Write) WithLogger(l *log.Logger) {
+func (p *write) WithLogger(l *log.Logger) {
 	if p.Debug && l != nil {
 		p.logger = log.New(l.Writer(), loggingPrefix, l.Flags())
 	} else if p.Debug {
@@ -212,7 +212,7 @@ func (p *Write) WithLogger(l *log.Logger) {
 	}
 }
 
-func (p *Write) write(e *formatters.EventMsg) error {
+func (p *write) write(e *formatters.EventMsg) error {
 	var b []byte
 	var err error
 	if len(p.Indent) > 0 {
@@ -230,6 +230,8 @@ func (p *Write) write(e *formatters.EventMsg) error {
 	return nil
 }
 
-func (p *Write) WithTargets(tcs map[string]*types.TargetConfig) {}
+func (p *write) WithTargets(tcs map[string]*types.TargetConfig) {}
+
+func (p *write) WithActions(act map[string]map[string]interface{}) {}
 
-func (p *Write) WithActions(act map[string]map[string]interface{}) {}
+func (p *write) WithProcessors(procs map[string]map[string]any) {}
diff --git a/formatters/event_write/event_write_test.go b/formatters/event_write/event_write_test.go
index 30543996..a2df5fc9 100644
--- a/formatters/event_write/event_write_test.go
+++ b/formatters/event_write/event_write_test.go
@@ -216,7 +216,7 @@ var testset = map[string]struct {
 
 func TestEventWrite(t *testing.T) {
 	for name, ts := range testset {
-		p := &Write{logger: log.New(io.Discard, "", 0)}
+		p := &write{logger: log.New(io.Discard, "", 0)}
 		err := p.Init(ts.processor)
 		if err != nil {
 			t.Errorf("failed to initialize processors: %v", err)
diff --git a/formatters/processors.go b/formatters/processors.go
index 9b0e1d7f..451620a0 100644
--- a/formatters/processors.go
+++ b/formatters/processors.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/itchyny/gojq"
 	"github.com/mitchellh/mapstructure"
+
 	"github.com/openconfig/gnmic/types"
 )
 
@@ -31,6 +32,7 @@ var EventProcessorTypes = []string{
 	"event-jq",
 	"event-merge",
 	"event-override-ts",
+	"event-rate-limit",
 	"event-strings",
 	"event-to-tag",
 	"event-trigger",
@@ -39,6 +41,7 @@ var EventProcessorTypes = []string{
 	"event-data-convert",
 	"event-value-tag",
 	"event-starlark",
+	"event-combine",
 }
 
 type Initializer func() EventProcessor
@@ -55,6 +58,7 @@ type EventProcessor interface {
 	WithTargets(map[string]*types.TargetConfig)
 	WithLogger(l *log.Logger)
 	WithActions(act map[string]map[string]interface{})
+	WithProcessors(procs map[string]map[string]any)
 }
 
 func DecodeConfig(src, dst interface{}) error {
@@ -88,33 +92,43 @@ func WithActions(acts map[string]map[string]interface{}) Option {
 	}
 }
 
+func WithProcessors(procs map[string]map[string]interface{}) Option {
+	return func(p EventProcessor) {
+		p.WithProcessors(procs)
+	}
+}
+
 func CheckCondition(code *gojq.Code, e *EventMsg) (bool, error) {
+	if code == nil {
+		return true, nil
+	}
+
 	var res interface{}
-	if code != nil {
-		input := make(map[string]interface{})
-		b, err := json.Marshal(e)
-		if err != nil {
-			return false, err
-		}
-		err = json.Unmarshal(b, &input)
-		if err != nil {
-			return false, err
-		}
-		iter := code.Run(input)
-		if err != nil {
-			return false, err
-		}
-		var ok bool
-		res, ok = iter.Next()
-		// iterator not done, so the final result won't be a boolean
-		if !ok {
-			//
-			return false, nil
-		}
-		if err, ok = res.(error); ok {
-			return false, err
-		}
+
+	input := make(map[string]interface{})
+	b, err := json.Marshal(e)
+	if err != nil {
+		return false, err
+	}
+	err = json.Unmarshal(b, &input)
+	if err != nil {
+		return false, err
+	}
+	iter := code.Run(input)
+	if err != nil {
+		return false, err
+	}
+	var ok bool
+	res, ok = iter.Next()
+	// iterator not done, so the final result won't be a boolean
+	if !ok {
+		//
+		return false, nil
 	}
+	if err, ok = res.(error); ok {
+		return false, err
+	}
+
 	switch res := res.(type) {
 	case bool:
 		return res, nil
diff --git a/go.mod b/go.mod
index baf00e2a..1b81f35c 100644
--- a/go.mod
+++ b/go.mod
@@ -7,10 +7,11 @@ require (
 	github.com/adrg/xdg v0.4.0
 	github.com/c-bata/go-prompt v0.2.5
 	github.com/damiannolan/sasl v1.0.0
-	github.com/docker/docker v24.0.6+incompatible
+	github.com/docker/docker v24.0.7+incompatible
 	github.com/fsnotify/fsnotify v1.6.0
 	github.com/fullstorydev/grpcurl v1.8.7
 	github.com/go-redis/redis/v8 v8.11.5
+	github.com/go-redsync/redsync/v4 v4.10.0
 	github.com/go-resty/resty/v2 v2.7.0
 	github.com/google/go-cmp v0.5.9
 	github.com/google/uuid v1.3.1
@@ -18,8 +19,10 @@ require (
 	github.com/gorilla/mux v1.8.0
 	github.com/gosnmp/gosnmp v1.35.0
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
+	github.com/guptarohit/asciigraph v0.5.6
 	github.com/hairyhenderson/gomplate/v3 v3.11.5
 	github.com/hashicorp/consul/api v1.25.1
+	github.com/hashicorp/golang-lru/v2 v2.0.7
 	github.com/huandu/xstrings v1.4.0
 	github.com/influxdata/influxdb-client-go/v2 v2.12.3
 	github.com/itchyny/gojq v0.12.13
@@ -31,7 +34,7 @@ require (
 	github.com/manifoldco/promptui v0.9.0
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/mitchellh/mapstructure v1.5.0
-	github.com/nats-io/nats.go v1.30.1
+	github.com/nats-io/nats.go v1.31.0
 	github.com/nats-io/stan.go v0.10.4
 	github.com/nsf/termbox-go v1.1.1
 	github.com/olekukonko/tablewriter v0.0.5
@@ -42,6 +45,7 @@ require (
 	github.com/prometheus/client_golang v1.16.0
 	github.com/prometheus/client_model v0.4.0
 	github.com/prometheus/prometheus v0.45.0
+	github.com/redis/go-redis/v9 v9.2.1
 	github.com/spf13/cobra v1.7.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.15.0
@@ -50,7 +54,7 @@ require (
 	golang.org/x/crypto v0.14.0
 	golang.org/x/oauth2 v0.12.0
 	golang.org/x/sync v0.3.0
-	google.golang.org/grpc v1.58.2
+	google.golang.org/grpc v1.59.0
 	google.golang.org/protobuf v1.31.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/yaml.v2 v2.4.0
@@ -60,7 +64,7 @@ require (
 )
 
 require (
-	cloud.google.com/go/compute v1.21.0 // indirect
+	cloud.google.com/go/compute v1.23.0 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	cloud.google.com/go/iam v1.1.1 // indirect
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
@@ -86,6 +90,7 @@ require (
 	github.com/go-openapi/jsonpointer v0.19.6 // indirect
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
 	github.com/go-openapi/swag v0.22.3 // indirect
+	github.com/gomodule/redigo v2.0.0+incompatible // indirect
 	github.com/google/gnostic v0.6.9 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/s2a-go v0.1.4 // indirect
@@ -114,7 +119,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/nats-io/jwt/v2 v2.4.1 // indirect
+	github.com/nats-io/jwt/v2 v2.5.2 // indirect
 	github.com/oklog/run v1.1.0 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
 	github.com/pierrec/lz4/v4 v4.1.18 // indirect
@@ -125,8 +130,8 @@ require (
 	golang.org/x/mod v0.11.0 // indirect
 	golang.org/x/term v0.13.0 // indirect
 	golang.org/x/tools v0.10.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	k8s.io/klog/v2 v2.100.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
@@ -136,7 +141,7 @@ require (
 )
 
 require (
-	cloud.google.com/go v0.110.4 // indirect
+	cloud.google.com/go v0.110.7 // indirect
 	cloud.google.com/go/storage v1.30.1 // indirect
 	github.com/AlekSi/pointer v1.2.0
 	github.com/Masterminds/goutils v1.1.1 // indirect
@@ -177,7 +182,7 @@ require (
 	github.com/go-git/go-billy/v5 v5.3.1 // indirect
 	github.com/go-git/go-git/v5 v5.4.2 // indirect
 	github.com/gogo/protobuf v1.3.2
-	github.com/golang/glog v1.1.0 // indirect
+	github.com/golang/glog v1.1.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/golang/snappy v0.0.4
@@ -195,7 +200,7 @@ require (
 	github.com/hashicorp/go-rootcerts v1.0.2 // indirect
 	github.com/hashicorp/go-sockaddr v1.0.2 // indirect
 	github.com/hashicorp/go-uuid v1.0.3 // indirect
-	github.com/hashicorp/golang-lru v0.6.0 // indirect
+	github.com/hashicorp/golang-lru v1.0.2 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/hashicorp/serf v0.10.1 // indirect
 	github.com/hashicorp/vault/api v1.6.0 // indirect
@@ -209,7 +214,7 @@ require (
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/joho/godotenv v1.4.0 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
-	github.com/klauspost/compress v1.17.0 // indirect
+	github.com/klauspost/compress v1.17.2 // indirect
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
@@ -217,9 +222,9 @@ require (
 	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/mattn/go-tty v0.0.4 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
-	github.com/nats-io/nats-server/v2 v2.9.20
+	github.com/nats-io/nats-server/v2 v2.10.4
 	github.com/nats-io/nats-streaming-server v0.24.3 // indirect
-	github.com/nats-io/nkeys v0.4.5 // indirect
+	github.com/nats-io/nkeys v0.4.6 // indirect
 	github.com/nats-io/nuid v1.0.1 // indirect
 	github.com/openconfig/grpctunnel v0.0.0-20220819142823-6f5422b8ca70
 	github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -252,7 +257,7 @@ require (
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/api v0.126.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect
+	google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
diff --git a/go.sum b/go.sum
index 255bf172..b873c875 100644
--- a/go.sum
+++ b/go.sum
@@ -31,8 +31,8 @@ cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Ud
 cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
 cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=
 cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
-cloud.google.com/go v0.110.4 h1:1JYyxKMN9hd5dR2MYTPWkGUgcoxVVhg0LKNKEo0qvmk=
-cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
+cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o=
+cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -43,8 +43,8 @@ cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTB
 cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw=
 cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
 cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
-cloud.google.com/go/compute v1.21.0 h1:JNBsyXVoOoNJtTQcnEY5uYpZIbeCTYIeDe0Xh1bySMk=
-cloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
+cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
+cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
@@ -236,6 +236,10 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
 github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg=
 github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
@@ -301,8 +305,8 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/
 github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
 github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
 github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE=
-github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
+github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
 github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -407,8 +411,14 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
+github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
+github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
 github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
 github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/go-redsync/redsync/v4 v4.10.0 h1:hTeAak4C73mNBQSTq6KCKDFaiIlfC+z5yTTl8fCJuBs=
+github.com/go-redsync/redsync/v4 v4.10.0/go.mod h1:ZfayzutkgeBmEmBlUR3j+rF6kN44UUGtEdfzhBFZTPc=
 github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
 github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -430,8 +440,8 @@ github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
-github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
+github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
+github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -470,6 +480,8 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
+github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
@@ -566,6 +578,8 @@ github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWf
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/guptarohit/asciigraph v0.5.6 h1:0tra3HEhfdj1sP/9IedrCpfSiXYTtHdCgBhBL09Yx6E=
+github.com/guptarohit/asciigraph v0.5.6/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
 github.com/hairyhenderson/go-fsimpl v0.0.0-20220529183339-9deae3e35047 h1:nSSfN9G8O8XXDqB3aDEHJ8K+0llYYToNlTcWOe1Pti8=
 github.com/hairyhenderson/go-fsimpl v0.0.0-20220529183339-9deae3e35047/go.mod h1:30RY4Ey+bg+BGKBufZE2IEmxk7hok9U9mjdgZYomwN4=
 github.com/hairyhenderson/gomplate/v3 v3.11.5 h1:LSDxCw8tWC/ltOzbZaleUNjGJOIEgnR/SN3GM9eClsA=
@@ -643,8 +657,10 @@ github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
-github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
+github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
@@ -780,8 +796,8 @@ github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
 github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
-github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
+github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
@@ -899,21 +915,21 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
-github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4=
-github.com/nats-io/jwt/v2 v2.4.1/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
+github.com/nats-io/jwt/v2 v2.5.2 h1:DhGH+nKt+wIkDxM6qnVSKjokq5t59AZV5HRcFW0zJwU=
+github.com/nats-io/jwt/v2 v2.5.2/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
 github.com/nats-io/nats-server/v2 v2.7.4/go.mod h1:1vZ2Nijh8tcyNe8BDVyTviCd9NYzRbubQYiEHsvOQWc=
-github.com/nats-io/nats-server/v2 v2.9.20 h1:bt1dW6xsL1hWWwv7Hovm+EJt5L6iplyqlgEFkoEUk0k=
-github.com/nats-io/nats-server/v2 v2.9.20/go.mod h1:aTb/xtLCGKhfTFLxP591CMWfkdgBmcUUSkiSOe5A3gw=
+github.com/nats-io/nats-server/v2 v2.10.4 h1:uB9xcwon3tPXWAdmTJqqqC6cie3yuPWHJjjTBgaPNus=
+github.com/nats-io/nats-server/v2 v2.10.4/go.mod h1:eWm2JmHP9Lqm2oemB6/XGi0/GwsZwtWf8HIPUsh+9ns=
 github.com/nats-io/nats-streaming-server v0.24.3 h1:uZez8jBkXscua++jaDsK7DhpSAkizdetar6yWbPMRco=
 github.com/nats-io/nats-streaming-server v0.24.3/go.mod h1:rqWfyCbxlhKj//fAp8POdQzeADwqkVhZcoWlbhkuU5w=
 github.com/nats-io/nats.go v1.13.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
 github.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
 github.com/nats-io/nats.go v1.22.1/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA=
-github.com/nats-io/nats.go v1.30.1 h1:o5RND+GaKgzNm2IOSLmHunWs6vH0GooAAaZZitiGJWk=
-github.com/nats-io/nats.go v1.30.1/go.mod h1:dcfhUgmQNN4GJEfIb2f9R7Fow+gzBF4emzDHrVBd5qM=
+github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=
+github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
 github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
-github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk=
-github.com/nats-io/nkeys v0.4.5/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
+github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY=
+github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts=
 github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/nats-io/stan.go v0.10.2/go.mod h1:vo2ax8K2IxaR3JtEMLZRFKIdoK/3o1/PKueapB7ezX0=
@@ -1007,6 +1023,10 @@ github.com/prometheus/prometheus v0.45.0/go.mod h1:jC5hyO8ItJBnDWGecbEucMyXjzxGv
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
+github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
+github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
+github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
 github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -1079,6 +1099,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
+github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
 github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
@@ -1680,12 +1702,12 @@ google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2
 google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
 google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
-google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0=
-google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
-google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
+google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
+google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
+google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
+google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
 google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -1717,8 +1739,8 @@ google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzI
 google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
 google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I=
-google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
+google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
+google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
diff --git a/inputs/kafka_input/kafka_input.go b/inputs/kafka_input/kafka_input.go
index 8f06c2a8..5d717870 100644
--- a/inputs/kafka_input/kafka_input.go
+++ b/inputs/kafka_input/kafka_input.go
@@ -9,6 +9,7 @@
 package kafka_input
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
@@ -22,12 +23,13 @@ import (
 	"github.com/Shopify/sarama"
 	"github.com/damiannolan/sasl/oauthbearer"
 	"github.com/google/uuid"
+	"google.golang.org/protobuf/proto"
+
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/inputs"
 	"github.com/openconfig/gnmic/outputs"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
-	"google.golang.org/protobuf/proto"
 )
 
 const (
@@ -44,6 +46,9 @@ const (
 
 var defaultVersion = sarama.V2_5_0_0
 
+var openSquareBracket = []byte("[")
+var openCurlyBrace = []byte("{")
+
 func init() {
 	inputs.Register("kafka", func() inputs.Input {
 		return &KafkaInput{
@@ -162,8 +167,16 @@ START:
 			}
 			switch k.Cfg.Format {
 			case "event":
+				m.Value = bytes.TrimSpace(m.Value)
 				evMsgs := make([]*formatters.EventMsg, 1)
-				err = json.Unmarshal(m.Value, &evMsgs)
+				switch {
+				case len(m.Value) == 0:
+					continue
+				case m.Value[0] == openSquareBracket[0]:
+					err = json.Unmarshal(m.Value, &evMsgs)
+				case m.Value[0] == openCurlyBrace[0]:
+					err = json.Unmarshal(m.Value, evMsgs[0])
+				}
 				if err != nil {
 					if k.Cfg.Debug {
 						k.logger.Printf("%s failed to unmarshal event msg: %v", workerLogPrefix, err)
diff --git a/loaders/consul_loader/consul_loader.go b/loaders/consul_loader/consul_loader.go
index 49e98f88..9af78244 100644
--- a/loaders/consul_loader/consul_loader.go
+++ b/loaders/consul_loader/consul_loader.go
@@ -106,6 +106,8 @@ type serviceDef struct {
 	Name   string                 `mapstructure:"name,omitempty" json:"name,omitempty"`
 	Tags   []string               `mapstructure:"tags,omitempty" json:"tags,omitempty"`
 	Config map[string]interface{} `mapstructure:"config,omitempty" json:"config,omitempty"`
+
+	tags map[string]struct{}
 }
 
 func (c *consulLoader) Init(ctx context.Context, cfg map[string]interface{}, logger *log.Logger, opts ...loaders.Option) error {
@@ -124,6 +126,14 @@ func (c *consulLoader) Init(ctx context.Context, cfg map[string]interface{}, log
 		c.logger.SetOutput(logger.Writer())
 		c.logger.SetFlags(logger.Flags())
 	}
+
+	for _, se := range c.cfg.Services {
+		se.tags = make(map[string]struct{})
+		for _, t := range se.Tags {
+			se.tags[t] = struct{}{}
+		}
+	}
+
 	err = c.readVars(ctx)
 	if err != nil {
 		return err
@@ -152,7 +162,7 @@ func (c *consulLoader) Init(ctx context.Context, cfg map[string]interface{}, log
 		return fmt.Errorf("unknown action name %q", actName)
 	}
 	c.numActions = len(c.addActions) + len(c.delActions)
-	c.logger.Printf("intialized consul loader: %+v", c.cfg)
+	c.logger.Printf("initialized consul loader: %+v", c.cfg)
 	return nil
 }
 
@@ -361,23 +371,40 @@ func (c *consulLoader) serviceEntryToTargetConfig(se *api.ServiceEntry) (*types.
 	if se.Service == nil {
 		return tc, nil
 	}
+
+SRV:
 	for _, sd := range c.cfg.Services {
+		// match service name
 		if se.Service.Service == sd.Name {
-			if sd.Config != nil {
-				err := mapstructure.Decode(sd.Config, tc)
-				if err != nil {
-					return nil, err
+			continue
+		}
+
+		// match service tags
+		if len(sd.tags) > 0 {
+			for _, t := range se.Service.Tags {
+				if _, ok := sd.tags[t]; !ok {
+					goto SRV
 				}
 			}
-			tc.Address = se.Service.Address
-			if tc.Address == "" {
-				tc.Address = se.Node.Address
+		}
+
+		// decode config if present
+		if sd.Config != nil {
+			err := mapstructure.Decode(sd.Config, tc)
+			if err != nil {
+				return nil, err
 			}
-			tc.Address = net.JoinHostPort(tc.Address, strconv.Itoa(se.Service.Port))
-			tc.Name = se.Service.ID
-			return tc, nil
 		}
+
+		tc.Address = se.Service.Address
+		if tc.Address == "" {
+			tc.Address = se.Node.Address
+		}
+		tc.Address = net.JoinHostPort(tc.Address, strconv.Itoa(se.Service.Port))
+		tc.Name = se.Service.ID
+		return tc, nil
 	}
+
 	return nil, nil
 }
 
diff --git a/lockers/all/all.go b/lockers/all/all.go
index d4fab594..e2d99a0f 100644
--- a/lockers/all/all.go
+++ b/lockers/all/all.go
@@ -11,4 +11,5 @@ package all
 import (
 	_ "github.com/openconfig/gnmic/lockers/consul_locker"
 	_ "github.com/openconfig/gnmic/lockers/k8s_locker"
+	_ "github.com/openconfig/gnmic/lockers/redis_locker"
 )
diff --git a/lockers/locker.go b/lockers/locker.go
index 535ee311..bd9a6b03 100644
--- a/lockers/locker.go
+++ b/lockers/locker.go
@@ -17,27 +17,44 @@ import (
 	"github.com/mitchellh/mapstructure"
 )
 
-var (
-	ErrCanceled = errors.New("canceled")
-)
+var ErrCanceled = errors.New("canceled")
 
 type Locker interface {
+	// Init initialises the locker data, with the given configuration read from flags/files.
 	Init(context.Context, map[string]interface{}, ...Option) error
+	// Stop is called when the locker instance is called. It should unlock all aquired locks.
+	Stop() error
+	SetLogger(*log.Logger)
+
+	// This is the Target locking logic.
 
+	// Lock acquires a lock on given key.
 	Lock(context.Context, string, []byte) (bool, error)
+	// KeepLock maintains the lock on the target.
 	KeepLock(context.Context, string) (chan struct{}, chan error)
+	// IsLocked replys if the target given as string is currently locked or not.
 	IsLocked(context.Context, string) (bool, error)
+	// Unlock unlocks the target log.
 	Unlock(context.Context, string) error
 
+	// This is the instance registration logic.
+
+	// Register registers this instance in the registry. It must also maintain the registration (called in a goroutine from the main). ServiceRegistration.ID contains the ID of the service to register.
 	Register(context.Context, *ServiceRegistration) error
+	// Deregister removes this instance from the registry. This looks like it's not called.
 	Deregister(string) error
 
-	List(context.Context, string) (map[string]string, error)
+	// GetServices must return the gnmic instances.
 	GetServices(ctx context.Context, serviceName string, tags []string) ([]*Service, error)
+	// WatchServices must push all existing discovered gnmic instances
+	// into the provided channel.
 	WatchServices(ctx context.Context, serviceName string, tags []string, ch chan<- []*Service, dur time.Duration) error
 
-	Stop() error
-	SetLogger(*log.Logger)
+	// Mixed registration/target lock functions
+
+	// List returns all locks that start with prefix string,
+	// indexed by the lock name. Could be target locks or leader lock. It must return a map of matching keys to instance name.
+	List(ctx context.Context, prefix string) (map[string]string, error)
 }
 
 type Initializer func() Locker
@@ -55,6 +72,7 @@ func WithLogger(logger *log.Logger) Option {
 var LockerTypes = []string{
 	"consul",
 	"k8s",
+	"redis",
 }
 
 func Register(name string, initFn Initializer) {
diff --git a/lockers/redis_locker/redis_locker.go b/lockers/redis_locker/redis_locker.go
new file mode 100644
index 00000000..a2da63ba
--- /dev/null
+++ b/lockers/redis_locker/redis_locker.go
@@ -0,0 +1,257 @@
+package redis_locker
+
+import (
+	"context"
+	"crypto/rand"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"sync"
+	"time"
+
+	"github.com/go-redsync/redsync/v4"
+	"github.com/go-redsync/redsync/v4/redis/goredis/v9"
+	"github.com/openconfig/gnmic/lockers"
+	"github.com/openconfig/gnmic/utils"
+	goredislib "github.com/redis/go-redis/v9"
+)
+
+const (
+	defaultLeaseDuration = 10 * time.Second
+	defaultRetryTimer    = 2 * time.Second
+	defaultPollTimer     = 10 * time.Second
+	loggingPrefix        = "[redis_locker] "
+)
+
+func init() {
+	lockers.Register("redis", func() lockers.Locker {
+		return &redisLocker{
+			Cfg:             &config{},
+			m:               new(sync.RWMutex),
+			acquiredLocks:   make(map[string]*redsync.Mutex),
+			attemptingLocks: make(map[string]*redsync.Mutex),
+			registerLock:    make(map[string]context.CancelFunc),
+			logger:          log.New(io.Discard, loggingPrefix, utils.DefaultLoggingFlags),
+		}
+	})
+}
+
+type redisLocker struct {
+	Cfg             *config
+	logger          *log.Logger
+	m               *sync.RWMutex
+	acquiredLocks   map[string]*redsync.Mutex
+	attemptingLocks map[string]*redsync.Mutex
+	registerLock    map[string]context.CancelFunc
+
+	client      goredislib.UniversalClient
+	redisLocker *redsync.Redsync
+}
+
+type config struct {
+	Servers       []string      `mapstructure:"servers,omitempty" json:"servers,omitempty"`
+	MasterName    string        `mapstructure:"master-name,omitempty" json:"master-name,omitempty"`
+	Password      string        `mapstructure:"password,omitempty" json:"password,omitempty"`
+	LeaseDuration time.Duration `mapstructure:"lease-duration,omitempty" json:"lease-duration,omitempty"`
+	RenewPeriod   time.Duration `mapstructure:"renew-period,omitempty" json:"renew-period,omitempty"`
+	RetryTimer    time.Duration `mapstructure:"retry-timer,omitempty" json:"retry-timer,omitempty"`
+	PollTimer     time.Duration `mapstructure:"poll-timer,omitempty" json:"poll-timer,omitempty"`
+	Debug         bool          `mapstructure:"debug,omitempty" json:"debug,omitempty"`
+}
+
+func (k *redisLocker) Init(ctx context.Context, cfg map[string]interface{}, opts ...lockers.Option) error {
+	err := lockers.DecodeConfig(cfg, k.Cfg)
+	if err != nil {
+		return err
+	}
+	for _, opt := range opts {
+		opt(k)
+	}
+	err = k.setDefaults()
+	if err != nil {
+		return err
+	}
+	k.client = goredislib.NewUniversalClient(&goredislib.UniversalOptions{
+		Addrs:      k.Cfg.Servers,
+		MasterName: k.Cfg.MasterName,
+		Password:   k.Cfg.Password,
+	})
+	if err := k.client.Ping(ctx).Err(); err != nil {
+		return fmt.Errorf("cannot contact redis server: %w", err)
+	}
+	k.redisLocker = redsync.New(goredis.NewPool(k.client))
+	return nil
+}
+
+func (k *redisLocker) Lock(ctx context.Context, key string, val []byte) (bool, error) {
+	if k.Cfg.Debug {
+		k.logger.Printf("attempting to lock=%s", key)
+	}
+	mu := k.redisLocker.NewMutex(
+		key,
+		redsync.WithGenValueFunc(func() (string, error) {
+			rand, err := k.genRandValue()
+			if err != nil {
+				return "", err
+			}
+			return fmt.Sprintf("%s-%s", val, rand), nil
+		}),
+		redsync.WithExpiry(k.Cfg.LeaseDuration),
+	)
+	k.m.Lock()
+	k.attemptingLocks[key] = mu
+	k.m.Unlock()
+	defer func() {
+		k.m.Lock()
+		defer k.m.Unlock()
+		delete(k.attemptingLocks, key)
+	}()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return false, ctx.Err()
+		default:
+			err := mu.LockContext(ctx)
+			if err != nil {
+				switch err.(type) {
+				case *redsync.ErrTaken:
+					if k.Cfg.Debug {
+						k.logger.Printf("lock already taken lock=%s: %v", key, err)
+					}
+					return false, nil
+				default:
+					return false, fmt.Errorf("failed to acquire lock=%s: %w", key, err)
+				}
+			}
+
+			k.m.Lock()
+			k.acquiredLocks[key] = mu
+			k.m.Unlock()
+			return true, nil
+		}
+	}
+}
+
+func (k *redisLocker) KeepLock(ctx context.Context, key string) (chan struct{}, chan error) {
+	doneChan := make(chan struct{})
+	errChan := make(chan error)
+
+	go func() {
+		defer close(doneChan)
+		ticker := time.NewTicker(k.Cfg.RenewPeriod)
+		k.m.RLock()
+		lock, ok := k.acquiredLocks[key]
+		k.m.RUnlock()
+		for {
+			select {
+			case <-ctx.Done():
+				errChan <- ctx.Err()
+				return
+			case <-doneChan:
+				return
+			case <-ticker.C:
+				if !ok {
+					errChan <- fmt.Errorf("unable to maintain lock %q: not found in acquiredlocks", key)
+					return
+				}
+				ok, err := lock.ExtendContext(ctx)
+				if err != nil {
+					errChan <- err
+					return
+				}
+				if !ok {
+					errChan <- fmt.Errorf("could not keep lock")
+					return
+				}
+
+			}
+		}
+	}()
+	return doneChan, errChan
+}
+
+func (k *redisLocker) Unlock(ctx context.Context, key string) error {
+	k.m.Lock()
+	defer k.m.Unlock()
+	if lock, ok := k.acquiredLocks[key]; ok {
+		delete(k.acquiredLocks, key)
+		ok, err := lock.Unlock()
+		if err != nil {
+			return err
+		}
+		if !ok {
+			return fmt.Errorf("failed to unlock lock %s", key)
+		}
+	}
+	if lock, ok := k.attemptingLocks[key]; ok {
+		delete(k.attemptingLocks, key)
+		_, err := lock.Unlock()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (k *redisLocker) Stop() error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	keys := []string{}
+	k.m.RLock()
+	for key := range k.acquiredLocks {
+		keys = append(keys, key)
+	}
+	k.m.RUnlock()
+	for _, key := range keys {
+		k.Unlock(ctx, key)
+	}
+	return k.Deregister("")
+}
+
+func (k *redisLocker) SetLogger(logger *log.Logger) {
+	if logger != nil && k.logger != nil {
+		k.logger.SetOutput(logger.Writer())
+		k.logger.SetFlags(logger.Flags())
+	}
+}
+
+// helpers
+
+func (k *redisLocker) setDefaults() error {
+	if k.Cfg.LeaseDuration <= 0 {
+		k.Cfg.LeaseDuration = defaultLeaseDuration
+	}
+	if k.Cfg.RenewPeriod <= 0 || k.Cfg.RenewPeriod >= k.Cfg.LeaseDuration {
+		k.Cfg.RenewPeriod = k.Cfg.LeaseDuration / 2
+	}
+	if k.Cfg.RetryTimer <= 0 {
+		k.Cfg.RetryTimer = defaultRetryTimer
+	}
+	if k.Cfg.PollTimer <= 0 {
+		k.Cfg.PollTimer = defaultPollTimer
+	}
+	return nil
+}
+
+func (k *redisLocker) String() string {
+	b, err := json.Marshal(k.Cfg)
+	if err != nil {
+		return ""
+	}
+	return string(b)
+}
+
+// genRandValue is required to generate a random value
+// so that the redislock algorithm works properly
+// especially in multi-server setups.
+func (k *redisLocker) genRandValue() (string, error) {
+	b := make([]byte, 16)
+	_, err := rand.Read(b)
+	if err != nil {
+		return "", err
+	}
+	return base64.StdEncoding.EncodeToString(b), nil
+}
diff --git a/lockers/redis_locker/redis_registration.go b/lockers/redis_locker/redis_registration.go
new file mode 100644
index 00000000..3154127a
--- /dev/null
+++ b/lockers/redis_locker/redis_registration.go
@@ -0,0 +1,309 @@
+package redis_locker
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/go-redsync/redsync/v4"
+	"github.com/openconfig/gnmic/lockers"
+	goredis "github.com/redis/go-redis/v9"
+)
+
+// defaultWatchTimeout
+const defaultWatchTimeout = 10 * time.Second
+
+// redisRegistration represents a gnmic endpoint in redis.
+// It's serialised in the redis value to allow recovering
+// it during service discovery.
+type redisRegistration struct {
+	ID      string
+	Address string
+	Port    int
+	Tags    []string
+	Rand    string
+}
+
+func (k *redisLocker) Register(ctx context.Context, s *lockers.ServiceRegistration) error {
+	ctx, cancel := context.WithCancel(ctx)
+	k.m.Lock()
+	k.registerLock[s.ID] = cancel
+	k.m.Unlock()
+	if k.Cfg.Debug {
+		k.logger.Printf("locking service=%s", s.ID)
+	}
+	mutex := k.redisLocker.NewMutex(
+		fmt.Sprintf("%s-%s", s.Name, s.ID),
+		redsync.WithGenValueFunc(func() (string, error) {
+			rand, err := k.genRandValue()
+			if err != nil {
+				return "", err
+			}
+			reg := &redisRegistration{
+				ID:      s.ID,
+				Address: s.Address,
+				Port:    s.Port,
+				Tags:    s.Tags,
+				Rand:    rand,
+			}
+			val, err := json.Marshal(reg)
+			if err != nil {
+				return "", err
+			}
+			return string(val), nil
+		}),
+		redsync.WithExpiry(s.TTL),
+	)
+
+	err := mutex.LockContext(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to lock service=%s, %w", s.ID, err)
+	}
+
+	ticker := time.NewTicker(s.TTL / 2)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-ticker.C:
+			ok, err := mutex.ExtendContext(ctx)
+			if err != nil {
+				return fmt.Errorf("failed to extend lock for service=%s: %w", s.ID, err)
+			}
+			if !ok {
+				return fmt.Errorf("could not extend lock for service=%s", s.ID)
+			}
+		case <-ctx.Done():
+			mutex.Unlock()
+			return nil
+		}
+	}
+}
+
+func (k *redisLocker) Deregister(s string) error {
+	k.m.Lock()
+	defer k.m.Unlock()
+	for sid, lockCancel := range k.registerLock {
+		if k.Cfg.Debug {
+			k.logger.Printf("unlocking service=%s", sid)
+		}
+		lockCancel()
+		delete(k.registerLock, sid)
+	}
+	return nil
+}
+
+func (k *redisLocker) WatchServices(ctx context.Context, serviceName string, tags []string, sChan chan<- []*lockers.Service, watchTimeout time.Duration) error {
+	if watchTimeout <= 0 {
+		watchTimeout = defaultWatchTimeout
+	}
+	var err error
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+			if k.Cfg.Debug {
+				k.logger.Printf("(re)starting watch service=%q", serviceName)
+			}
+			err = k.watch(ctx, serviceName, tags, sChan, watchTimeout)
+			if err != nil {
+				k.logger.Printf("watch ended with error: %s", err)
+				time.Sleep(k.Cfg.RetryTimer)
+				continue
+			}
+
+			time.Sleep(k.Cfg.PollTimer)
+		}
+	}
+}
+
+func (k *redisLocker) watch(ctx context.Context, serviceName string, tags []string, sChan chan<- []*lockers.Service, watchTimeout time.Duration) error {
+	// timeoutSeconds := int64(watchTimeout.Seconds())
+	// TODO: implement watch
+	services, err := k.GetServices(ctx, serviceName, tags)
+	if err != nil {
+		return err
+	}
+
+	sChan <- services
+	return nil
+}
+
+func (k *redisLocker) getBatchOfKeys(ctx context.Context, key string, batchSize int64, cursor uint64) (uint64, map[string]*goredis.StringCmd, error) {
+	keys, cursor, err := k.client.Scan(
+		ctx,
+		cursor,
+		key,
+		batchSize,
+	).Result()
+	if err != nil {
+		return 0, nil, fmt.Errorf("failed to scan keys: %w", err)
+	}
+
+	results := map[string]*goredis.StringCmd{}
+	_, err = k.client.Pipelined(ctx, func(p goredis.Pipeliner) error {
+		for _, k := range keys {
+			results[k] = p.Get(ctx, k)
+		}
+		return nil
+	})
+
+	if err != nil {
+		return cursor, nil, fmt.Errorf("error getting contents of keys")
+	}
+
+	return cursor, results, nil
+}
+
+func (k *redisLocker) GetServices(ctx context.Context, serviceName string, tags []string) ([]*lockers.Service, error) {
+	var pageSize int64 = 50
+	var cursor uint64
+	var err error
+	var cmds map[string]*goredis.StringCmd
+	discoveredServiceRegistrations := []*redisRegistration{}
+	for {
+		select {
+		case <-ctx.Done():
+			return nil, ctx.Err()
+		default:
+			// to select all gnmic instances, matching the given prefix
+			cursor, cmds, err = k.getBatchOfKeys(
+				ctx,
+				fmt.Sprintf("%s-*", serviceName),
+				pageSize,
+				cursor,
+			)
+
+			if err != nil {
+				return nil, err
+			}
+			for _, cmd := range cmds {
+				bytesVal, err := cmd.Bytes()
+				if err != nil {
+					// key removed from redis
+					// could be that it has expired
+					// doesn't make a difference, we skip it
+					continue
+				}
+				serviceRegistration := &redisRegistration{}
+				if err := json.Unmarshal(bytesVal, serviceRegistration); err != nil {
+					// we don't have the data we expect
+					// skip it
+					continue
+				}
+
+				discoveredServiceRegistrations = append(
+					discoveredServiceRegistrations,
+					serviceRegistration,
+				)
+			}
+			// termination condition for redis scan
+			if cursor == 0 {
+				if k.Cfg.Debug {
+					k.logger.Printf("got %d services from redis", len(discoveredServiceRegistrations))
+				}
+				// convert discovered servicesRegistrations to services
+				discoveredServices := make([]*lockers.Service, len(discoveredServiceRegistrations))
+				for i, registration := range discoveredServiceRegistrations {
+					// match the required tags
+					if !matchTags(registration.Tags, tags) {
+						continue
+					}
+					discoveredServices[i] = &lockers.Service{
+						ID:   registration.ID,
+						Tags: registration.Tags,
+						Address: fmt.Sprintf(
+							"%s:%d",
+							registration.Address,
+							registration.Port,
+						),
+					}
+				}
+				return discoveredServices, nil
+			}
+		}
+	}
+}
+
+func (k *redisLocker) IsLocked(ctx context.Context, key string) (bool, error) {
+	count, err := k.client.Exists(ctx, key).Result()
+	if err != nil {
+		return false, fmt.Errorf("error during redis query: %w", err)
+	}
+
+	if count > 0 {
+		return true, nil
+	}
+	return false, nil
+}
+
+func (k *redisLocker) List(ctx context.Context, prefix string) (map[string]string, error) {
+	var cursor uint64
+	var err error
+	var cmds map[string]*goredis.StringCmd
+	data := map[string]string{}
+	for {
+		select {
+		case <-ctx.Done():
+			return nil, ctx.Err()
+		default:
+		}
+		cursor, cmds, err = k.getBatchOfKeys(
+			ctx,
+			fmt.Sprintf("%s*", prefix),
+			100,
+			cursor,
+		)
+		if err != nil {
+			return nil, fmt.Errorf("failed to fetch from redis: %w", err)
+		}
+		if k.Cfg.Debug {
+			k.logger.Printf(
+				"got %d keys from redis for prefix=%s",
+				len(cmds),
+				prefix,
+			)
+		}
+		for key, cmd := range cmds {
+			bytesVal, err := cmd.Bytes()
+			if err != nil {
+				// key removed from redis
+				// could be that it has expired
+				// doesn't make a difference, we skip it
+				continue
+			}
+			// we add a random string at the end of the value for redis
+			// redlock algorithm, so we need to remove it here
+			lastIndex := bytes.LastIndex(bytesVal, []byte("-"))
+			// if it's not there, we skip the key
+			if lastIndex < 0 {
+				continue
+			}
+			data[key] = string(bytesVal[:lastIndex])
+		}
+
+		if cursor == 0 {
+			return data, nil
+		}
+	}
+}
+
+func matchTags(tags, wantedTags []string) bool {
+	if wantedTags == nil {
+		return true
+	}
+	tagsMap := map[string]struct{}{}
+
+	for _, t := range tags {
+		tagsMap[t] = struct{}{}
+	}
+
+	for _, wt := range wantedTags {
+		if _, ok := tagsMap[wt]; !ok {
+			return false
+		}
+	}
+	return true
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index 79e28714..457bda74 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -67,11 +67,13 @@ nav:
           - TCP: user_guide/outputs/tcp_output.md
           - UDP: user_guide/outputs/udp_output.md
           - SNMP: user_guide/outputs/snmp_output.md
+          - ASCII Graph: user_guide/outputs/asciigraph_output.md
           
       - Processors: 
           - Introduction: user_guide/event_processors/intro.md
           - Add Tag: user_guide/event_processors/event_add_tag.md
           - Allow: user_guide/event_processors/event_allow.md
+          - Combine: user_guide/event_processors/event_combine.md
           - Convert: user_guide/event_processors/event_convert.md
           - Data Convert: user_guide/event_processors/event_data_convert.md
           - Date string: user_guide/event_processors/event_date_string.md
@@ -83,6 +85,7 @@ nav:
           - JQ: user_guide/event_processors/event_jq.md
           - Merge: user_guide/event_processors/event_merge.md
           - Override TS: user_guide/event_processors/event_override_ts.md
+          - Rate Limit: user_guide/event_processors/event_rate_limit.md
           - Starlark: user_guide/event_processors/event_starlark.md
           - Strings: user_guide/event_processors/event_strings.md
           - To Tag: user_guide/event_processors/event_to_tag.md
diff --git a/outputs/all/all.go b/outputs/all/all.go
index 619aa3e2..80ce0bb5 100644
--- a/outputs/all/all.go
+++ b/outputs/all/all.go
@@ -9,6 +9,7 @@
 package all
 
 import (
+	_ "github.com/openconfig/gnmic/outputs/asciigraph_output"
 	_ "github.com/openconfig/gnmic/outputs/file"
 	_ "github.com/openconfig/gnmic/outputs/gnmi_output"
 	_ "github.com/openconfig/gnmic/outputs/influxdb_output"
diff --git a/outputs/asciigraph_output/asciigraph.go b/outputs/asciigraph_output/asciigraph.go
new file mode 100644
index 00000000..6a14e30c
--- /dev/null
+++ b/outputs/asciigraph_output/asciigraph.go
@@ -0,0 +1,537 @@
+// © 2023 Nokia.
+//
+// This code is a Contribution to the gNMIc project (“Work”) made under the Google Software Grant and Corporate Contributor License Agreement (“CLA”) and governed by the Apache License 2.0.
+// No other rights or licenses in or to any of Nokia’s intellectual property are granted for any other purpose.
+// This code is provided on an “as is” basis without any warranties of any kind.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package asciigraph_output
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"math"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+	"text/template"
+	"time"
+
+	"github.com/guptarohit/asciigraph"
+	"github.com/nsf/termbox-go"
+	"github.com/openconfig/gnmi/proto/gnmi"
+	"github.com/prometheus/client_golang/prometheus"
+	"google.golang.org/protobuf/proto"
+
+	"github.com/openconfig/gnmic/formatters"
+	"github.com/openconfig/gnmic/outputs"
+	"github.com/openconfig/gnmic/types"
+	"github.com/openconfig/gnmic/utils"
+)
+
+const (
+	loggingPrefix       = "[asciigraph_output:%s] "
+	defaultRefreshTimer = time.Second
+	defaultPrecision    = 2
+	defaultTimeout      = 10 * time.Second
+)
+
+var (
+	defaultLabelColor   = asciigraph.Blue
+	defaultCaptionColor = asciigraph.Default
+	defaultAxisColor    = asciigraph.Default
+)
+
+func init() {
+	outputs.Register("asciigraph", func() outputs.Output {
+		return &asciigraphOutput{
+			cfg:     &cfg{},
+			logger:  log.New(io.Discard, loggingPrefix, utils.DefaultLoggingFlags),
+			eventCh: make(chan *formatters.EventMsg, 100),
+			m:       new(sync.RWMutex),
+			data:    make(map[string]*series),
+			colors:  make(map[asciigraph.AnsiColor]struct{}),
+		}
+	})
+}
+
+// asciigraphOutput //
+type asciigraphOutput struct {
+	cfg     *cfg
+	logger  *log.Logger
+	eventCh chan *formatters.EventMsg
+
+	m       *sync.RWMutex
+	data    map[string]*series
+	colors  map[asciigraph.AnsiColor]struct{}
+	caption string
+
+	captionColor asciigraph.AnsiColor
+	axisColor    asciigraph.AnsiColor
+	labelColor   asciigraph.AnsiColor
+	evps         []formatters.EventProcessor
+
+	targetTpl *template.Template
+}
+
+type series struct {
+	name  string
+	data  []float64
+	color asciigraph.AnsiColor
+}
+
+// cfg //
+type cfg struct {
+	// The caption to be displayed under the graph
+	Caption string `mapstructure:"caption,omitempty" json:"caption,omitempty"`
+	// The graph height
+	Height int `mapstructure:"height,omitempty" json:"height,omitempty"`
+	// The graph width
+	Width int `mapstructure:"width,omitempty" json:"width,omitempty"`
+	// The graph minimum value for the vertical axis
+	LowerBound *float64 `mapstructure:"lower-bound,omitempty" json:"lower-bound,omitempty"`
+	// the graph maximum value for the vertical axis
+	UpperBound *float64 `mapstructure:"upper-bound,omitempty" json:"upper-bound,omitempty"`
+	// The graph offset
+	Offset int `mapstructure:"offset,omitempty" json:"offset,omitempty"`
+	// The decimal point precision of the label values
+	Precision uint `mapstructure:"precision,omitempty" json:"precision,omitempty"`
+	// The caption color
+	CaptionColor string `mapstructure:"caption-color,omitempty" json:"caption-color,omitempty"`
+	// The axis color
+	AxisColor string `mapstructure:"axis-color,omitempty" json:"axis-color,omitempty"`
+	// The label color
+	LabelColor string `mapstructure:"label-color,omitempty" json:"label-color,omitempty"`
+	// The graph refresh timer
+	RefreshTimer time.Duration `mapstructure:"refresh-timer,omitempty" json:"refresh-timer,omitempty"`
+	// Add target the received subscribe responses
+	AddTarget string `mapstructure:"add-target,omitempty" json:"add-target,omitempty"`
+	//
+	TargetTemplate string `mapstructure:"target-template,omitempty" json:"target-template,omitempty"`
+	// list of event processors
+	EventProcessors []string `mapstructure:"event-processors,omitempty" json:"event-processors,omitempty"`
+	// enable extra logging
+	Debug bool `mapstructure:"debug,omitempty" json:"debug,omitempty"`
+}
+
+func (a *asciigraphOutput) String() string {
+	b, err := json.Marshal(a.cfg)
+	if err != nil {
+		return ""
+	}
+	return string(b)
+}
+
+func (a *asciigraphOutput) SetEventProcessors(ps map[string]map[string]interface{},
+	logger *log.Logger,
+	tcs map[string]*types.TargetConfig,
+	acts map[string]map[string]interface{}) {
+	for _, epName := range a.cfg.EventProcessors {
+		if epCfg, ok := ps[epName]; ok {
+			epType := ""
+			for k := range epCfg {
+				epType = k
+				break
+			}
+			if in, ok := formatters.EventProcessors[epType]; ok {
+				ep := in()
+				err := ep.Init(epCfg[epType],
+					formatters.WithLogger(logger),
+					formatters.WithTargets(tcs),
+					formatters.WithActions(acts),
+				)
+				if err != nil {
+					a.logger.Printf("failed initializing event processor '%s' of type='%s': %v", epName, epType, err)
+					continue
+				}
+				a.evps = append(a.evps, ep)
+				a.logger.Printf("added event processor '%s' of type=%s to file output", epName, epType)
+				continue
+			}
+			a.logger.Printf("%q event processor has an unknown type=%q", epName, epType)
+			continue
+		}
+		a.logger.Printf("%q event processor not found!", epName)
+	}
+}
+
+func (a *asciigraphOutput) SetLogger(logger *log.Logger) {
+	if logger != nil && a.logger != nil {
+		a.logger.SetOutput(logger.Writer())
+		a.logger.SetFlags(logger.Flags())
+	}
+}
+
+// Init //
+func (a *asciigraphOutput) Init(ctx context.Context, name string, cfg map[string]interface{}, opts ...outputs.Option) error {
+	err := outputs.DecodeConfig(cfg, a.cfg)
+	if err != nil {
+		return err
+	}
+
+	a.logger.SetPrefix(fmt.Sprintf(loggingPrefix, name))
+
+	for _, opt := range opts {
+		opt(a)
+	}
+
+	if a.cfg.TargetTemplate == "" {
+		a.targetTpl = outputs.DefaultTargetTemplate
+	} else if a.cfg.AddTarget != "" {
+		a.targetTpl, err = utils.CreateTemplate("target-template", a.cfg.TargetTemplate)
+		if err != nil {
+			return err
+		}
+		a.targetTpl = a.targetTpl.Funcs(outputs.TemplateFuncs)
+	}
+	// set defaults
+	err = a.setDefaults()
+	if err != nil {
+		return err
+	}
+	//
+	go a.graph(ctx)
+	a.logger.Printf("initialized asciigraph output: %s", a.String())
+	return nil
+}
+
+func (a *asciigraphOutput) setDefaults() error {
+	a.labelColor = defaultLabelColor
+	if a.cfg.LabelColor != "" {
+		if lc, ok := asciigraph.ColorNames[a.cfg.LabelColor]; ok {
+			a.labelColor = lc
+		} else {
+			return fmt.Errorf("unknown label color %s", a.cfg.LabelColor)
+		}
+	}
+
+	a.captionColor = defaultCaptionColor
+	if a.cfg.CaptionColor != "" {
+		if lc, ok := asciigraph.ColorNames[a.cfg.CaptionColor]; ok {
+			a.captionColor = lc
+		} else {
+			return fmt.Errorf("unknown caption color %s", a.cfg.CaptionColor)
+		}
+	}
+
+	a.axisColor = defaultAxisColor
+	if a.cfg.AxisColor != "" {
+		if lc, ok := asciigraph.ColorNames[a.cfg.AxisColor]; ok {
+			a.axisColor = lc
+		} else {
+			return fmt.Errorf("unknown axis color %s", a.cfg.AxisColor)
+		}
+
+	}
+
+	if a.cfg.RefreshTimer <= 0 {
+		a.cfg.RefreshTimer = defaultRefreshTimer
+	}
+	if a.cfg.Precision <= 0 {
+		a.cfg.Precision = defaultPrecision
+	}
+
+	return a.getTermSize()
+}
+
+// Write //
+func (a *asciigraphOutput) Write(ctx context.Context, rsp proto.Message, meta outputs.Meta) {
+	if rsp == nil {
+		return
+	}
+
+	subRsp, err := outputs.AddSubscriptionTarget(rsp, meta, a.cfg.AddTarget, a.targetTpl)
+	if err != nil {
+		a.logger.Printf("failed to add target to the response: %v", err)
+		return
+	}
+	evs, err := formatters.ResponseToEventMsgs(meta["subscription-name"], subRsp, meta, a.evps...)
+	if err != nil {
+		a.logger.Printf("failed to convert messages to events: %v", err)
+		return
+	}
+	for _, ev := range evs {
+		a.WriteEvent(ctx, ev)
+	}
+}
+
+func (a *asciigraphOutput) WriteEvent(ctx context.Context, ev *formatters.EventMsg) {
+	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
+	defer cancel()
+	select {
+	case <-ctx.Done():
+		a.logger.Printf("write timeout: %v", ctx.Err())
+	case a.eventCh <- ev:
+	}
+}
+
+// Close //
+func (a *asciigraphOutput) Close() error {
+	return nil
+}
+
+// Metrics //
+func (a *asciigraphOutput) RegisterMetrics(reg *prometheus.Registry) {
+}
+
+func (a *asciigraphOutput) SetName(name string) {}
+
+func (a *asciigraphOutput) SetClusterName(name string) {}
+
+func (a *asciigraphOutput) SetTargetsConfig(map[string]*types.TargetConfig) {}
+
+func (a *asciigraphOutput) graph(ctx context.Context) {
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case ev, ok := <-a.eventCh:
+			if !ok {
+				return
+			}
+			a.plot(ev)
+		case <-time.After(a.cfg.RefreshTimer):
+			a.plot(nil)
+		}
+	}
+}
+
+func (a *asciigraphOutput) plot(e *formatters.EventMsg) {
+	a.m.Lock()
+	defer a.m.Unlock()
+	a.getTermSize()
+	if e != nil && len(e.Values) > 0 {
+		a.updateData(e)
+	}
+
+	data, colors := a.buildData()
+	if len(data) == 0 {
+		return
+	}
+	opts := []asciigraph.Option{
+		asciigraph.Height(a.cfg.Height),
+		asciigraph.Width(a.cfg.Width),
+		asciigraph.Offset(a.cfg.Offset),
+		asciigraph.Precision(a.cfg.Precision),
+		asciigraph.Caption(a.caption),
+		asciigraph.CaptionColor(a.captionColor),
+		asciigraph.SeriesColors(colors...),
+		asciigraph.AxisColor(a.axisColor),
+		asciigraph.LabelColor(a.labelColor),
+	}
+	if a.cfg.LowerBound != nil {
+		opts = append(opts, asciigraph.LowerBound(*a.cfg.LowerBound))
+	}
+	if a.cfg.UpperBound != nil {
+		opts = append(opts, asciigraph.UpperBound(*a.cfg.UpperBound))
+	}
+	plot := asciigraph.PlotMany(data, opts...)
+	asciigraph.Clear()
+	fmt.Fprintln(os.Stdout, plot)
+}
+
+func (a *asciigraphOutput) updateData(e *formatters.EventMsg) {
+	if e == nil {
+		return
+	}
+	evs := splitEvent(e)
+	for _, ev := range evs {
+		sn := a.buildSeriesName(e)
+		serie := a.getOrCreateSerie(sn)
+		for _, v := range ev.Values {
+			i, err := toFloat(v)
+			if err != nil {
+				continue
+			}
+			serie.data = append(serie.data, i)
+			break
+		}
+	}
+}
+
+func (a *asciigraphOutput) getOrCreateSerie(name string) *series {
+	serie, ok := a.data[name]
+	if ok {
+		return serie
+	}
+	color := a.pickColor()
+	serie = &series{
+		name:  name,
+		data:  make([]float64, 0, a.cfg.Width-a.cfg.Offset),
+		color: color,
+	}
+	a.data[name] = serie
+	a.colors[serie.color] = struct{}{}
+
+	a.setCaption()
+	return serie
+}
+
+func (a *asciigraphOutput) setCaption() {
+	seriesNames := make([]string, 0, len(a.data))
+	for seriesName := range a.data {
+		seriesNames = append(seriesNames, seriesName)
+	}
+	sort.Strings(seriesNames)
+	a.caption = ""
+	if a.cfg.Debug {
+		a.caption = fmt.Sprintf("(h=%d,w=%d)\n", a.cfg.Height, a.cfg.Width)
+	}
+	a.caption = fmt.Sprintf("%s\n", a.cfg.Caption)
+
+	for _, sn := range seriesNames {
+		color := a.data[sn].color
+		a.caption += color.String() + "-+- " + sn + asciigraph.Default.String() + "\n"
+	}
+}
+
+func (a *asciigraphOutput) buildData() ([][]float64, []asciigraph.AnsiColor) {
+	numgraphs := len(a.data)
+	series := make([]*series, 0, numgraphs)
+	// sort series by name
+	for _, serie := range a.data {
+		size := len(serie.data)
+		if size == 0 {
+			continue
+		}
+		if size > a.cfg.Width {
+			serie.data = serie.data[size-a.cfg.Width:]
+		}
+		series = append(series, serie)
+	}
+	sort.Slice(series,
+		func(i, j int) bool {
+			return series[i].name < series[j].name
+		})
+
+	data := make([][]float64, 0, numgraphs)
+	colors := make([]asciigraph.AnsiColor, 0, numgraphs)
+	// get float slices and colors
+	for _, serie := range series {
+		data = append(data, serie.data)
+		colors = append(colors, serie.color)
+	}
+	return data, colors
+}
+
+func splitEvent(e *formatters.EventMsg) []*formatters.EventMsg {
+	numVals := len(e.Values)
+	switch numVals {
+	case 0:
+		return nil
+	case 1:
+		return []*formatters.EventMsg{e}
+	}
+
+	evs := make([]*formatters.EventMsg, 0, numVals)
+	for k, v := range e.Values {
+		ev := &formatters.EventMsg{
+			Name:      e.Name,
+			Timestamp: e.Timestamp,
+			Tags:      e.Tags,
+			Values:    map[string]interface{}{k: v},
+		}
+		evs = append(evs, ev)
+	}
+	return evs
+}
+
+func (a *asciigraphOutput) buildSeriesName(e *formatters.EventMsg) string {
+	sb := &strings.Builder{}
+	sb.WriteString(e.Name)
+	sb.WriteString(":")
+	for k := range e.Values {
+		sb.WriteString(k)
+	}
+	numTags := len(e.Tags)
+	if numTags == 0 {
+		return sb.String()
+	}
+	sb.WriteString("{")
+	tagNames := make([]string, 0, numTags)
+	for k := range e.Tags {
+		tagNames = append(tagNames, k)
+	}
+	sort.Strings(tagNames)
+	for i, tn := range tagNames {
+		fmt.Fprintf(sb, "%s=%s", tn, e.Tags[tn])
+		if numTags != i+1 {
+			sb.WriteString(", ")
+		}
+	}
+	sb.WriteString("}")
+	return sb.String()
+}
+
+func toFloat(v interface{}) (float64, error) {
+	switch i := v.(type) {
+	case float64:
+		return float64(i), nil
+	case float32:
+		return float64(i), nil
+	case int64:
+		return float64(i), nil
+	case int32:
+		return float64(i), nil
+	case int16:
+		return float64(i), nil
+	case int8:
+		return float64(i), nil
+	case uint64:
+		return float64(i), nil
+	case uint32:
+		return float64(i), nil
+	case uint16:
+		return float64(i), nil
+	case uint8:
+		return float64(i), nil
+	case int:
+		return float64(i), nil
+	case uint:
+		return float64(i), nil
+	case string:
+		f, err := strconv.ParseFloat(i, 64)
+		if err != nil {
+			return math.NaN(), err
+		}
+		return f, err
+		//lint:ignore SA1019 still need DecimalVal for backward compatibility
+	case *gnmi.Decimal64:
+		return float64(i.Digits) / math.Pow10(int(i.Precision)), nil
+	default:
+		return math.NaN(), errors.New("getFloat: unknown value is of incompatible type")
+	}
+}
+
+func (a *asciigraphOutput) pickColor() asciigraph.AnsiColor {
+	for _, c := range asciigraph.ColorNames {
+		if _, ok := a.colors[c]; !ok {
+			return c
+		}
+	}
+	return 0
+}
+
+func (a *asciigraphOutput) getTermSize() error {
+	err := termbox.Init()
+	if err != nil {
+		return fmt.Errorf("could not initialize a terminal box: %v", err)
+	}
+	w, h := termbox.Size()
+	termbox.Close()
+	if a.cfg.Width <= 0 || a.cfg.Width > w-10 {
+		a.cfg.Width = w - 10
+	}
+	numSeries := len(a.data)
+	if a.cfg.Height <= 0 || a.cfg.Height > h-(numSeries+1)-5 {
+		a.cfg.Height = h - (numSeries + 1) - 5
+	}
+	return nil
+}
diff --git a/outputs/file/file_output.go b/outputs/file/file_output.go
index e4312c33..6aad7590 100644
--- a/outputs/file/file_output.go
+++ b/outputs/file/file_output.go
@@ -19,13 +19,14 @@ import (
 	"text/template"
 	"time"
 
+	"github.com/prometheus/client_golang/prometheus"
+	"golang.org/x/sync/semaphore"
+	"google.golang.org/protobuf/proto"
+
 	"github.com/openconfig/gnmic/formatters"
 	"github.com/openconfig/gnmic/outputs"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
-	"github.com/prometheus/client_golang/prometheus"
-	"golang.org/x/sync/semaphore"
-	"google.golang.org/protobuf/proto"
 )
 
 const (
@@ -38,7 +39,7 @@ const (
 func init() {
 	outputs.Register("file", func() outputs.Output {
 		return &File{
-			Cfg:    &Config{},
+			cfg:    &Config{},
 			logger: log.New(io.Discard, loggingPrefix, utils.DefaultLoggingFlags),
 		}
 	})
@@ -46,7 +47,7 @@ func init() {
 
 // File //
 type File struct {
-	Cfg    *Config
+	cfg    *Config
 	file   *os.File
 	logger *log.Logger
 	mo     *formatters.MarshalOptions
@@ -78,7 +79,7 @@ type Config struct {
 }
 
 func (f *File) String() string {
-	b, err := json.Marshal(f)
+	b, err := json.Marshal(f.cfg)
 	if err != nil {
 		return ""
 	}
@@ -89,7 +90,7 @@ func (f *File) SetEventProcessors(ps map[string]map[string]interface{},
 	logger *log.Logger,
 	tcs map[string]*types.TargetConfig,
 	acts map[string]map[string]interface{}) {
-	for _, epName := range f.Cfg.EventProcessors {
+	for _, epName := range f.cfg.EventProcessors {
 		if epCfg, ok := ps[epName]; ok {
 			epType := ""
 			for k := range epCfg {
@@ -127,7 +128,7 @@ func (f *File) SetLogger(logger *log.Logger) {
 
 // Init //
 func (f *File) Init(ctx context.Context, name string, cfg map[string]interface{}, opts ...outputs.Option) error {
-	err := outputs.DecodeConfig(cfg, f.Cfg)
+	err := outputs.DecodeConfig(cfg, f.cfg)
 	if err != nil {
 		return err
 	}
@@ -137,24 +138,24 @@ func (f *File) Init(ctx context.Context, name string, cfg map[string]interface{}
 	for _, opt := range opts {
 		opt(f)
 	}
-	if f.Cfg.Format == "proto" {
+	if f.cfg.Format == "proto" {
 		return fmt.Errorf("proto format not supported in output type 'file'")
 	}
-	if f.Cfg.Separator == "" {
-		f.Cfg.Separator = defaultSeparator
+	if f.cfg.Separator == "" {
+		f.cfg.Separator = defaultSeparator
 	}
-	if f.Cfg.FileName == "" && f.Cfg.FileType == "" {
-		f.Cfg.FileType = "stdout"
+	if f.cfg.FileName == "" && f.cfg.FileType == "" {
+		f.cfg.FileType = "stdout"
 	}
 
-	switch f.Cfg.FileType {
+	switch f.cfg.FileType {
 	case "stdout":
 		f.file = os.Stdout
 	case "stderr":
 		f.file = os.Stderr
 	default:
 	CRFILE:
-		f.file, err = os.OpenFile(f.Cfg.FileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+		f.file, err = os.OpenFile(f.cfg.FileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 		if err != nil {
 			f.logger.Printf("failed to create file: %v", err)
 			time.Sleep(10 * time.Second)
@@ -162,41 +163,46 @@ func (f *File) Init(ctx context.Context, name string, cfg map[string]interface{}
 		}
 	}
 
-	if f.Cfg.Format == "" {
-		f.Cfg.Format = defaultFormat
+	if f.cfg.Format == "" {
+		f.cfg.Format = defaultFormat
 	}
-	if f.Cfg.FileType == "stdout" || f.Cfg.FileType == "stderr" {
-		f.Cfg.Indent = "  "
-		f.Cfg.Multiline = true
+	if f.cfg.FileType == "stdout" || f.cfg.FileType == "stderr" {
+		f.cfg.Indent = "  "
+		f.cfg.Multiline = true
 	}
-	if f.Cfg.Multiline && f.Cfg.Indent == "" {
-		f.Cfg.Indent = "  "
+	if f.cfg.Multiline && f.cfg.Indent == "" {
+		f.cfg.Indent = "  "
 	}
-	if f.Cfg.ConcurrencyLimit < 1 {
-		f.Cfg.ConcurrencyLimit = defaultWriteConcurrency
+	if f.cfg.ConcurrencyLimit < 1 {
+		switch f.cfg.FileType {
+		case "stdout", "stderr":
+			f.cfg.ConcurrencyLimit = 1
+		default:
+			f.cfg.ConcurrencyLimit = defaultWriteConcurrency
+		}
 	}
 
-	f.sem = semaphore.NewWeighted(int64(f.Cfg.ConcurrencyLimit))
+	f.sem = semaphore.NewWeighted(int64(f.cfg.ConcurrencyLimit))
 
 	f.mo = &formatters.MarshalOptions{
-		Multiline:        f.Cfg.Multiline,
-		Indent:           f.Cfg.Indent,
-		Format:           f.Cfg.Format,
-		OverrideTS:       f.Cfg.OverrideTimestamps,
-		CalculateLatency: f.Cfg.CalculateLatency,
+		Multiline:        f.cfg.Multiline,
+		Indent:           f.cfg.Indent,
+		Format:           f.cfg.Format,
+		OverrideTS:       f.cfg.OverrideTimestamps,
+		CalculateLatency: f.cfg.CalculateLatency,
 	}
-	if f.Cfg.TargetTemplate == "" {
+	if f.cfg.TargetTemplate == "" {
 		f.targetTpl = outputs.DefaultTargetTemplate
-	} else if f.Cfg.AddTarget != "" {
-		f.targetTpl, err = utils.CreateTemplate("target-template", f.Cfg.TargetTemplate)
+	} else if f.cfg.AddTarget != "" {
+		f.targetTpl, err = utils.CreateTemplate("target-template", f.cfg.TargetTemplate)
 		if err != nil {
 			return err
 		}
 		f.targetTpl = f.targetTpl.Funcs(outputs.TemplateFuncs)
 	}
 
-	if f.Cfg.MsgTemplate != "" {
-		f.msgTpl, err = utils.CreateTemplate(fmt.Sprintf("%s-msg-template", name), f.Cfg.MsgTemplate)
+	if f.cfg.MsgTemplate != "" {
+		f.msgTpl, err = utils.CreateTemplate(fmt.Sprintf("%s-msg-template", name), f.cfg.MsgTemplate)
 		if err != nil {
 			return err
 		}
@@ -227,13 +233,13 @@ func (f *File) Write(ctx context.Context, rsp proto.Message, meta outputs.Meta)
 	defer f.sem.Release(1)
 
 	numberOfReceivedMsgs.WithLabelValues(f.file.Name()).Inc()
-	rsp, err = outputs.AddSubscriptionTarget(rsp, meta, f.Cfg.AddTarget, f.targetTpl)
+	rsp, err = outputs.AddSubscriptionTarget(rsp, meta, f.cfg.AddTarget, f.targetTpl)
 	if err != nil {
 		f.logger.Printf("failed to add target to the response: %v", err)
 	}
-	bb, err := outputs.Marshal(rsp, meta, f.mo, f.Cfg.SplitEvents, f.evps...)
+	bb, err := outputs.Marshal(rsp, meta, f.mo, f.cfg.SplitEvents, f.evps...)
 	if err != nil {
-		if f.Cfg.Debug {
+		if f.cfg.Debug {
 			f.logger.Printf("failed marshaling proto msg: %v", err)
 		}
 		numberOfFailWriteMsgs.WithLabelValues(f.file.Name(), "marshal_error").Inc()
@@ -246,7 +252,7 @@ func (f *File) Write(ctx context.Context, rsp proto.Message, meta outputs.Meta)
 		if f.msgTpl != nil {
 			b, err = outputs.ExecTemplate(b, f.msgTpl)
 			if err != nil {
-				if f.Cfg.Debug {
+				if f.cfg.Debug {
 					log.Printf("failed to execute template: %v", err)
 				}
 				numberOfFailWriteMsgs.WithLabelValues(f.file.Name(), "template_error").Inc()
@@ -254,9 +260,9 @@ func (f *File) Write(ctx context.Context, rsp proto.Message, meta outputs.Meta)
 			}
 		}
 
-		n, err := f.file.Write(append(b, []byte(f.Cfg.Separator)...))
+		n, err := f.file.Write(append(b, []byte(f.cfg.Separator)...))
 		if err != nil {
-			if f.Cfg.Debug {
+			if f.cfg.Debug {
 				f.logger.Printf("failed to write to file '%s': %v", f.file.Name(), err)
 			}
 			numberOfFailWriteMsgs.WithLabelValues(f.file.Name(), "write_error").Inc()
@@ -267,7 +273,60 @@ func (f *File) Write(ctx context.Context, rsp proto.Message, meta outputs.Meta)
 	}
 }
 
-func (f *File) WriteEvent(ctx context.Context, ev *formatters.EventMsg) {}
+func (f *File) WriteEvent(ctx context.Context, ev *formatters.EventMsg) {
+	select {
+	case <-ctx.Done():
+		return
+	default:
+	}
+	var evs = []*formatters.EventMsg{ev}
+	for _, proc := range f.evps {
+		evs = proc.Apply(evs...)
+	}
+	toWrite := []byte{}
+	if f.cfg.SplitEvents {
+		for _, pev := range evs {
+			var err error
+			var b []byte
+			if f.cfg.Multiline {
+				b, err = json.MarshalIndent(pev, "", f.cfg.Indent)
+			} else {
+				b, err = json.Marshal(pev)
+			}
+			if err != nil {
+				fmt.Printf("failed to WriteEvent: %v", err)
+				numberOfFailWriteMsgs.WithLabelValues(f.file.Name(), "marshal_error").Inc()
+				return
+			}
+			toWrite = append(toWrite, b...)
+			toWrite = append(toWrite, []byte(f.cfg.Separator)...)
+		}
+	} else {
+		var err error
+		var b []byte
+		if f.cfg.Multiline {
+			b, err = json.MarshalIndent(evs, "", f.cfg.Indent)
+		} else {
+			b, err = json.Marshal(evs)
+		}
+		if err != nil {
+			fmt.Printf("failed to WriteEvent: %v", err)
+			numberOfFailWriteMsgs.WithLabelValues(f.file.Name(), "marshal_error").Inc()
+			return
+		}
+		toWrite = append(toWrite, b...)
+		toWrite = append(toWrite, []byte(f.cfg.Separator)...)
+	}
+
+	n, err := f.file.Write(toWrite)
+	if err != nil {
+		fmt.Printf("failed to WriteEvent: %v", err)
+		numberOfFailWriteMsgs.WithLabelValues(f.file.Name(), "write_error").Inc()
+		return
+	}
+	numberOfWrittenBytes.WithLabelValues(f.file.Name()).Add(float64(n))
+	numberOfWrittenMsgs.WithLabelValues(f.file.Name()).Inc()
+}
 
 // Close //
 func (f *File) Close() error {
@@ -277,7 +336,7 @@ func (f *File) Close() error {
 
 // Metrics //
 func (f *File) RegisterMetrics(reg *prometheus.Registry) {
-	if !f.Cfg.EnableMetrics {
+	if !f.cfg.EnableMetrics {
 		return
 	}
 	if err := registerMetrics(reg); err != nil {
diff --git a/outputs/output.go b/outputs/output.go
index db080c23..4b607828 100644
--- a/outputs/output.go
+++ b/outputs/output.go
@@ -19,13 +19,14 @@ import (
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/openconfig/gnmi/proto/gnmi"
+	"github.com/prometheus/client_golang/prometheus"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
+
 	"github.com/openconfig/gnmic/formatters"
 	_ "github.com/openconfig/gnmic/formatters/all"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
-	"github.com/prometheus/client_golang/prometheus"
-	"google.golang.org/protobuf/proto"
-	"google.golang.org/protobuf/reflect/protoreflect"
 )
 
 type Output interface {
@@ -60,6 +61,7 @@ var OutputTypes = map[string]struct{}{
 	"gnmi":             {},
 	"jetstream":        {},
 	"snmp":             {},
+	"asciigraph":       {},
 }
 
 func Register(name string, initFn Initializer) {
@@ -140,11 +142,11 @@ var (
 		template.New("target-template").
 			Funcs(TemplateFuncs).
 			Parse(defaultTargetTemplateString))
-)
 
-var TemplateFuncs = template.FuncMap{
-	"host": utils.GetHost,
-}
+	TemplateFuncs = template.FuncMap{
+		"host": utils.GetHost,
+	}
+)
 
 const (
 	defaultTargetTemplateString = `
diff --git a/outputs/prometheus_output/prometheus_common.go b/outputs/prometheus_output/prometheus_common.go
index 763c966a..e1adf883 100644
--- a/outputs/prometheus_output/prometheus_common.go
+++ b/outputs/prometheus_output/prometheus_common.go
@@ -18,9 +18,10 @@ import (
 	"time"
 
 	"github.com/openconfig/gnmi/proto/gnmi"
-	"github.com/openconfig/gnmic/formatters"
 	"github.com/prometheus/prometheus/model/labels"
 	"github.com/prometheus/prometheus/prompb"
+
+	"github.com/openconfig/gnmic/formatters"
 )
 
 const (
@@ -134,6 +135,7 @@ type NamedTimeSeries struct {
 func (m *MetricBuilder) TimeSeriesFromEvent(ev *formatters.EventMsg) []*NamedTimeSeries {
 	promTS := make([]*NamedTimeSeries, 0, len(ev.Values))
 	tsLabels := m.GetLabels(ev)
+	timestamp := ev.Timestamp / int64(time.Millisecond)
 	for k, v := range ev.Values {
 		fv, err := toFloat(v)
 		if err != nil {
@@ -143,18 +145,21 @@ func (m *MetricBuilder) TimeSeriesFromEvent(ev *formatters.EventMsg) []*NamedTim
 			fv = 1.0
 		}
 		tsName := m.MetricName(ev.Name, k)
+		tsLabelsWithName := make([]prompb.Label, 0, len(tsLabels)+1)
+		tsLabelsWithName = append(tsLabelsWithName, tsLabels...)
+		tsLabelsWithName = append(tsLabelsWithName,
+			prompb.Label{
+				Name:  labels.MetricName,
+				Value: tsName,
+			})
 		nts := &NamedTimeSeries{
 			Name: tsName,
 			TS: &prompb.TimeSeries{
-				Labels: append(tsLabels,
-					prompb.Label{
-						Name:  labels.MetricName,
-						Value: m.MetricName(ev.Name, k),
-					}),
+				Labels: tsLabelsWithName,
 				Samples: []prompb.Sample{
 					{
 						Value:     fv,
-						Timestamp: ev.Timestamp / int64(time.Millisecond),
+						Timestamp: timestamp,
 					},
 				},
 			},
diff --git a/outputs/prometheus_output/prometheus_common_test.go b/outputs/prometheus_output/prometheus_common_test.go
index 292972c9..e333b883 100644
--- a/outputs/prometheus_output/prometheus_common_test.go
+++ b/outputs/prometheus_output/prometheus_common_test.go
@@ -10,6 +10,9 @@ package prometheus_output
 
 import (
 	"testing"
+
+	"github.com/openconfig/gnmic/formatters"
+	"github.com/prometheus/prometheus/model/labels"
 )
 
 var metricNameSet = map[string]struct {
@@ -69,6 +72,31 @@ var metricNameSet = map[string]struct {
 	},
 }
 
+func TestTimeSeriesFromEvent(t *testing.T) {
+	metricBuilder := &MetricBuilder{StringsAsLabels: true}
+	event := &formatters.EventMsg{
+		Name:      "eventName",
+		Timestamp: 12345,
+		Tags: map[string]string{
+			"tagName": "tagVal",
+		},
+		Values: map[string]interface{}{
+			"strName1": "strVal1",
+			"strName2": "strVal2",
+			"intName1": 1,
+			"intName2": 2,
+		},
+		Deletes: []string{},
+	}
+	for _, nts := range metricBuilder.TimeSeriesFromEvent(event) {
+		for _, label := range nts.TS.Labels {
+			if label.Name == labels.MetricName && label.Value != nts.Name {
+				t.Errorf("__name__ label wrong, expected '%s', got '%s'", nts.Name, label.Value)
+			}
+		}
+	}
+}
+
 func TestMetricName(t *testing.T) {
 	for name, tc := range metricNameSet {
 		t.Run(name, func(t *testing.T) {
diff --git a/outputs/prometheus_output/prometheus_write_output/prometheus_write_client.go b/outputs/prometheus_output/prometheus_write_output/prometheus_write_client.go
index 5fc1120b..9353020f 100644
--- a/outputs/prometheus_output/prometheus_write_output/prometheus_write_client.go
+++ b/outputs/prometheus_output/prometheus_write_output/prometheus_write_client.go
@@ -11,15 +11,22 @@ package prometheus_write_output
 import (
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
+	"sort"
 	"time"
 
 	gogoproto "github.com/gogo/protobuf/proto"
 	"github.com/golang/snappy"
-	"github.com/openconfig/gnmic/utils"
 	"github.com/prometheus/prometheus/prompb"
+
+	"github.com/openconfig/gnmic/utils"
+)
+
+var (
+	ErrMarshal = errors.New("marshal error")
 )
 
 var backoff = 100 * time.Millisecond
@@ -97,6 +104,10 @@ WRITE:
 	if numTS == 0 {
 		return
 	}
+	// sort timeSeries by timestamp
+	sort.Slice(pts, func(i, j int) bool {
+		return pts[i].Samples[0].Timestamp < pts[j].Samples[0].Timestamp
+	})
 	chunk := make([]prompb.TimeSeries, 0, p.cfg.MaxTimeSeriesPerWrite)
 	for i, pt := range pts {
 		// append timeSeries to chunk
@@ -113,7 +124,9 @@ WRITE:
 				Timeseries: chunk,
 			})
 			if err != nil {
-				prometheusWriteNumberOfFailSendMsgs.WithLabelValues(err.Error()).Inc()
+				if p.cfg.Debug {
+					p.logger.Print(err)
+				}
 				continue
 			}
 			prometheusWriteSendDuration.Set(float64(time.Since(start).Nanoseconds()))
@@ -150,6 +163,7 @@ RETRY:
 			time.Sleep(backoff)
 			goto RETRY
 		}
+		prometheusWriteNumberOfFailSendMsgs.WithLabelValues("client_failure").Inc()
 		return err
 	}
 	defer rsp.Body.Close()
@@ -158,6 +172,7 @@ RETRY:
 		p.logger.Printf("got response from remote: status=%s", rsp.Status)
 	}
 	if rsp.StatusCode >= 300 {
+		prometheusWriteNumberOfFailSendMsgs.WithLabelValues(fmt.Sprintf("status_code=%d", rsp.StatusCode)).Inc()
 		msg, err := io.ReadAll(rsp.Body)
 		if err != nil {
 			return err
@@ -213,7 +228,9 @@ func (p *promWriteOutput) writeMetadata(ctx context.Context) {
 			Metadata: mds,
 		})
 		if err != nil {
-			prometheusWriteNumberOfFailSendMetadataMsgs.WithLabelValues(err.Error()).Inc()
+			if p.cfg.Debug {
+				p.logger.Print(err)
+			}
 			return
 		}
 		prometheusWriteMetadataSendDuration.Set(float64(time.Since(start).Nanoseconds()))
@@ -237,7 +254,9 @@ func (p *promWriteOutput) writeMetadata(ctx context.Context) {
 		Metadata: mds,
 	})
 	if err != nil {
-		prometheusWriteNumberOfFailSendMetadataMsgs.WithLabelValues(err.Error()).Inc()
+		if p.cfg.Debug {
+			p.logger.Print(err)
+		}
 		return
 	}
 	prometheusWriteMetadataSendDuration.Set(float64(time.Since(start).Nanoseconds()))
@@ -247,7 +266,8 @@ func (p *promWriteOutput) writeMetadata(ctx context.Context) {
 func (p *promWriteOutput) makeHTTPRequest(ctx context.Context, wr *prompb.WriteRequest) (*http.Request, error) {
 	b, err := gogoproto.Marshal(wr)
 	if err != nil {
-		return nil, fmt.Errorf("failed to marshal proto write request: %v", err)
+		prometheusWriteNumberOfFailSendMsgs.WithLabelValues("marshal_error").Inc()
+		return nil, fmt.Errorf("marshal error: %w", err)
 	}
 	compBytes := snappy.Encode(nil, b)
 	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, p.cfg.URL, bytes.NewBuffer(compBytes))
diff --git a/outputs/prometheus_output/prometheus_write_output/prometheus_write_output.go b/outputs/prometheus_output/prometheus_write_output/prometheus_write_output.go
index ad56ed6b..7844f500 100644
--- a/outputs/prometheus_output/prometheus_write_output/prometheus_write_output.go
+++ b/outputs/prometheus_output/prometheus_write_output/prometheus_write_output.go
@@ -16,20 +16,21 @@ import (
 	"io"
 	"log"
 	"net/http"
+	"net/url"
 	"sync"
 	"text/template"
 	"time"
 
 	"github.com/openconfig/gnmi/proto/gnmi"
-	"github.com/openconfig/gnmic/formatters"
-	"github.com/openconfig/gnmic/outputs"
-	"github.com/openconfig/gnmic/types"
-	"github.com/openconfig/gnmic/utils"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/prometheus/prompb"
+	"google.golang.org/protobuf/proto"
 
+	"github.com/openconfig/gnmic/formatters"
+	"github.com/openconfig/gnmic/outputs"
 	promcom "github.com/openconfig/gnmic/outputs/prometheus_output"
-	"google.golang.org/protobuf/proto"
+	"github.com/openconfig/gnmic/types"
+	"github.com/openconfig/gnmic/utils"
 )
 
 const (
@@ -44,6 +45,7 @@ const (
 	defaultMetricHelp                 = "gNMIc generated metric"
 	userAgent                         = "gNMIc prometheus write"
 	defaultNumWorkers                 = 1
+	defaultNumWriters                 = 1
 )
 
 func init() {
@@ -104,6 +106,7 @@ type config struct {
 	StringsAsLabels        bool     `mapstructure:"strings-as-labels,omitempty" json:"strings-as-labels,omitempty"`
 	EventProcessors        []string `mapstructure:"event-processors,omitempty" json:"event-processors,omitempty"`
 	NumWorkers             int      `mapstructure:"num-workers,omitempty" json:"num-workers,omitempty"`
+	NumWriters             int      `mapstructure:"num-writers,omitempty" json:"num-writers,omitempty"`
 	EnableMetrics          bool     `mapstructure:"enable-metrics,omitempty" json:"enable-metrics,omitempty"`
 }
 
@@ -131,6 +134,10 @@ func (p *promWriteOutput) Init(ctx context.Context, name string, cfg map[string]
 	if p.cfg.URL == "" {
 		return errors.New("missing url field")
 	}
+	_, err = url.Parse(p.cfg.URL)
+	if err != nil {
+		return err
+	}
 	if p.cfg.Name == "" {
 		p.cfg.Name = name
 	}
@@ -172,8 +179,9 @@ func (p *promWriteOutput) Init(ctx context.Context, name string, cfg map[string]
 	for i := 0; i < p.cfg.NumWorkers; i++ {
 		go p.worker(ctx)
 	}
-
-	go p.writer(ctx)
+	for i := 0; i < p.cfg.NumWriters; i++ {
+		go p.writer(ctx)
+	}
 	go p.metadataWriter(ctx)
 	p.logger.Printf("initialized prometheus write output %s: %s", p.cfg.Name, p.String())
 	return nil
@@ -371,6 +379,9 @@ func (p *promWriteOutput) setDefaults() error {
 	if p.cfg.NumWorkers <= 0 {
 		p.cfg.NumWorkers = defaultNumWorkers
 	}
+	if p.cfg.NumWriters <= 0 {
+		p.cfg.NumWriters = defaultNumWriters
+	}
 	if p.cfg.MaxTimeSeriesPerWrite <= 0 {
 		p.cfg.MaxTimeSeriesPerWrite = defaultMaxTSPerWrite
 	}
diff --git a/target/target.go b/target/target.go
index 67e7e6e0..5abedf11 100644
--- a/target/target.go
+++ b/target/target.go
@@ -19,12 +19,13 @@ import (
 	"github.com/jhump/protoreflect/desc"
 	"github.com/openconfig/gnmi/proto/gnmi"
 	"github.com/openconfig/gnmi/proto/gnmi_ext"
-	"github.com/openconfig/gnmic/types"
 	"golang.org/x/net/proxy"
 	"golang.org/x/oauth2"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/oauth"
 	"google.golang.org/grpc/metadata"
+
+	"github.com/openconfig/gnmic/types"
 )
 
 type TargetError struct {
diff --git a/types/subscription.go b/types/subscription.go
index 6f23a52b..20052c8d 100644
--- a/types/subscription.go
+++ b/types/subscription.go
@@ -29,7 +29,7 @@ type SubscriptionConfig struct {
 	Paths               []string              `mapstructure:"paths,omitempty" json:"paths,omitempty"`
 	Mode                string                `mapstructure:"mode,omitempty" json:"mode,omitempty"`
 	StreamMode          string                `mapstructure:"stream-mode,omitempty" json:"stream-mode,omitempty"`
-	Encoding            string                `mapstructure:"encoding,omitempty" json:"encoding,omitempty"`
+	Encoding            *string               `mapstructure:"encoding,omitempty" json:"encoding,omitempty"`
 	Qos                 *uint32               `mapstructure:"qos,omitempty" json:"qos,omitempty"`
 	SampleInterval      *time.Duration        `mapstructure:"sample-interval,omitempty" json:"sample-interval,omitempty"`
 	HeartbeatInterval   *time.Duration        `mapstructure:"heartbeat-interval,omitempty" json:"heartbeat-interval,omitempty"`
diff --git a/types/target.go b/types/target.go
index 7475f645..0f769f32 100644
--- a/types/target.go
+++ b/types/target.go
@@ -16,13 +16,14 @@ import (
 	"strings"
 	"time"
 
-	"github.com/openconfig/gnmic/utils"
 	"golang.org/x/oauth2"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/credentials/insecure"
 	"google.golang.org/grpc/credentials/oauth"
 	"google.golang.org/grpc/encoding/gzip"
+
+	"github.com/openconfig/gnmic/utils"
 )
 
 // TargetConfig //
@@ -34,7 +35,7 @@ type TargetConfig struct {
 	AuthScheme    string            `mapstructure:"auth-scheme,omitempty" json:"auth-scheme,omitempty" yaml:"auth-scheme,omitempty"`
 	Timeout       time.Duration     `mapstructure:"timeout,omitempty" json:"timeout,omitempty" yaml:"timeout,omitempty"`
 	Insecure      *bool             `mapstructure:"insecure,omitempty" json:"insecure,omitempty" yaml:"insecure,omitempty"`
-	TLSCA         *string           `mapstructure:"tls-ca,omitempty" json:"tls-ca,omitempty" yaml:"tlsca,omitempty"`
+	TLSCA         *string           `mapstructure:"tls-ca,omitempty" json:"tls-ca,omitempty" yaml:"tls-ca,omitempty"`
 	TLSCert       *string           `mapstructure:"tls-cert,omitempty" json:"tls-cert,omitempty" yaml:"tls-cert,omitempty"`
 	TLSKey        *string           `mapstructure:"tls-key,omitempty" json:"tls-key,omitempty" yaml:"tls-key,omitempty"`
 	SkipVerify    *bool             `mapstructure:"skip-verify,omitempty" json:"skip-verify,omitempty" yaml:"skip-verify,omitempty"`
@@ -55,7 +56,8 @@ type TargetConfig struct {
 	Token         *string           `mapstructure:"token,omitempty" json:"token,omitempty" yaml:"token,omitempty"`
 	Proxy         string            `mapstructure:"proxy,omitempty" json:"proxy,omitempty" yaml:"proxy,omitempty"`
 	//
-	TunnelTargetType string `mapstructure:"-" json:"tunnel-target-type,omitempty" yaml:"tunnel-target-type,omitempty"`
+	TunnelTargetType string  `mapstructure:"-" json:"tunnel-target-type,omitempty" yaml:"tunnel-target-type,omitempty"`
+	Encoding         *string `mapstructure:"encoding,omitempty" yaml:"encoding,omitempty" json:"encoding,omitempty"`
 }
 
 func (tc TargetConfig) String() string {