Repository: flant/shell-operator
Pull request #345 (2021-12-29 09:47:07)

feat(debug): command to get snapshots
<!--
Thank you for sending a pull request! Here some tips for contributors:

1. Fill the description template below.
2. Include appropriate tests (if necessary). Make sure that all CI checks passed.
3. If the Pull Request is a work in progress, make use of GitHub's "Draft PR" feature and mark it as such.
-->

#### Overview

- debug command to get hook list
- debug command to get hook snapshots
- print snapshots info in log on every hook run


<!-- Describe your changes briefly here. -->

#### What this PR does / why we need it

Make hooks debugging a little easy.

<!--
- Please state in detail why we need this PR and what it solves.
- If your PR closes some of the existing issues, please add links to them here.
  Mentioned issues will be automatically closed.
  Usage: "Closes #<issue number>", or "Closes (paste link of issue)"
-->

#### Special notes for your reviewer

#### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->

```release-note

```


Patch:
diff --git a/Dockerfile b/Dockerfile
index 63b297d..139ec5b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,7 +29,7 @@ ARG TARGETPLATFORM
 RUN apk --no-cache add ca-certificates bash sed tini && \
     kubectlArch=$(echo ${TARGETPLATFORM:-linux/amd64} | sed 's/\/v7//') && \
     echo "Download kubectl for ${kubectlArch}" && \
-    wget https://storage.googleapis.com/kubernetes-release/release/v1.19.11/bin/${kubectlArch}/kubectl -O /bin/kubectl && \
+    wget https://storage.googleapis.com/kubernetes-release/release/v1.21.8/bin/${kubectlArch}/kubectl -O /bin/kubectl && \
     chmod +x /bin/kubectl && \
     mkdir /hooks
 ADD frameworks/shell /frameworks/shell
diff --git a/cmd/shell-operator/main.go b/cmd/shell-operator/main.go
index f6f1977..b01ef76 100644
--- a/cmd/shell-operator/main.go
+++ b/cmd/shell-operator/main.go
@@ -62,6 +62,7 @@ func main() {
 	app.DefineStartCommandFlags(kpApp, startCmd)
 
 	debug.DefineDebugCommands(kpApp)
+	debug.DefineDebugCommandsSelf(kpApp)
 
 	kingpin.MustParse(kpApp.Parse(os.Args[1:]))
 }
diff --git a/pkg/debug/debug-cmd.go b/pkg/debug/debug-cmd.go
index 74ef374..16721b8 100644
--- a/pkg/debug/debug-cmd.go
+++ b/pkg/debug/debug-cmd.go
@@ -11,21 +11,33 @@ import (
 var OutputFormat = "text"
 
 func DefineDebugCommands(kpApp *kingpin.Application) {
-	// Queue mamanging commands
-	queueCmd := app.CommandWithDefaultUsageTemplate(kpApp, "queue", "Manage queues.")
+	// Queue dump commands.
+	queueCmd := app.CommandWithDefaultUsageTemplate(kpApp, "queue", "Dump queues.")
 
 	queueListCmd := queueCmd.Command("list", "Dump tasks in all queues.").
 		Action(func(c *kingpin.ParseContext) error {
-			queueDump, err := Queue(DefaultClient()).Dump(OutputFormat)
+			out, err := Queue(DefaultClient()).List(OutputFormat)
 			if err != nil {
 				return err
 			}
-			fmt.Println(string(queueDump))
+			fmt.Println(string(out))
 			return nil
 		})
 	AddOutputJsonYamlTextFlag(queueListCmd)
 	app.DefineDebugUnixSocketFlag(queueListCmd)
 
+	queueMainCmd := queueCmd.Command("main", "Dump tasks in the main queue.").
+		Action(func(c *kingpin.ParseContext) error {
+			out, err := Queue(DefaultClient()).Main(OutputFormat)
+			if err != nil {
+				return err
+			}
+			fmt.Println(string(out))
+			return nil
+		})
+	AddOutputJsonYamlTextFlag(queueMainCmd)
+	app.DefineDebugUnixSocketFlag(queueMainCmd)
+
 	// Raw request command
 	var rawUrl string
 	rawCommand := app.CommandWithDefaultUsageTemplate(kpApp, "raw", "Make a raw request to debug endpoint.").
@@ -42,6 +54,37 @@ func DefineDebugCommands(kpApp *kingpin.Application) {
 	app.DefineDebugUnixSocketFlag(rawCommand)
 }
 
+func DefineDebugCommandsSelf(kpApp *kingpin.Application) {
+	// Get hook names
+	hookCmd := app.CommandWithDefaultUsageTemplate(kpApp, "hook", "Actions for hooks")
+	hookListCmd := hookCmd.Command("list", "List all hooks.").
+		Action(func(c *kingpin.ParseContext) error {
+			outBytes, err := Hook(DefaultClient()).List(OutputFormat)
+			if err != nil {
+				return err
+			}
+			fmt.Println(string(outBytes))
+			return nil
+		})
+	AddOutputJsonYamlTextFlag(hookListCmd)
+	app.DefineDebugUnixSocketFlag(hookListCmd)
+
+	// Get hook snapshots
+	var hookName string
+	hookSnapshotCmd := hookCmd.Command("snapshot", "Dump hook snapshots.").
+		Action(func(c *kingpin.ParseContext) error {
+			outBytes, err := Hook(DefaultClient()).Name(hookName).Snapshots(OutputFormat)
+			if err != nil {
+				return err
+			}
+			fmt.Println(string(outBytes))
+			return nil
+		})
+	hookSnapshotCmd.Arg("hook_name", "").Required().StringVar(&hookName)
+	AddOutputJsonYamlTextFlag(hookSnapshotCmd)
+	app.DefineDebugUnixSocketFlag(hookSnapshotCmd)
+}
+
 func AddOutputJsonYamlTextFlag(cmd *kingpin.CmdClause) {
 	cmd.Flag("output", "Output format: json|yaml|text.").Short('o').
 		Default("text").
@@ -58,7 +101,36 @@ func Queue(client *Client) *QueueRequest {
 	}
 }
 
-func (qr *QueueRequest) Dump(format string) ([]byte, error) {
+func (qr *QueueRequest) List(format string) ([]byte, error) {
 	url := fmt.Sprintf("http://unix/queue/list.%s", format)
 	return qr.client.Get(url)
 }
+
+func (qr *QueueRequest) Main(format string) ([]byte, error) {
+	url := fmt.Sprintf("http://unix/queue/main.%s", format)
+	return qr.client.Get(url)
+}
+
+type HookRequest struct {
+	client *Client
+	name   string
+}
+
+func Hook(client *Client) *HookRequest {
+	return &HookRequest{client: client}
+}
+
+func (r *HookRequest) Name(name string) *HookRequest {
+	r.name = name
+	return r
+}
+
+func (r *HookRequest) List(format string) ([]byte, error) {
+	url := fmt.Sprintf("http://unix/hook/list.%s", format)
+	return r.client.Get(url)
+}
+
+func (r *HookRequest) Snapshots(format string) ([]byte, error) {
+	url := fmt.Sprintf("http://unix/hook/%s/snapshots.%s", r.name, format)
+	return r.client.Get(url)
+}
diff --git a/pkg/debug/server.go b/pkg/debug/server.go
index a1acd70..317e204 100644
--- a/pkg/debug/server.go
+++ b/pkg/debug/server.go
@@ -1,15 +1,17 @@
 package debug
 
 import (
+	"encoding/json"
+	"fmt"
 	"net"
 	"net/http"
 	"os"
 	"path"
 
-	log "github.com/sirupsen/logrus"
-
 	"github.com/go-chi/chi"
 	"github.com/go-chi/chi/middleware"
+	log "github.com/sirupsen/logrus"
+	"sigs.k8s.io/yaml"
 
 	"github.com/flant/shell-operator/pkg/app"
 	utils "github.com/flant/shell-operator/pkg/utils/file"
@@ -79,3 +81,64 @@ func (s *Server) Init() (err error) {
 
 	return nil
 }
+
+func (s *Server) Route(pattern string, handler func(request *http.Request) (interface{}, error)) {
+	// Should not happen.
+	if handler == nil {
+		return
+	}
+	s.Router.Get(pattern, func(writer http.ResponseWriter, request *http.Request) {
+		out, err := handler(request)
+
+		if err != nil {
+			writer.WriteHeader(http.StatusInternalServerError)
+			_, _ = fmt.Fprintf(writer, "Error: %s", err)
+			return
+		}
+
+		format := chi.URLParam(request, "format")
+		if format == "" {
+			format = "text"
+		}
+		structured_logger.GetLogEntry(request).Debugf("use format '%s'", format)
+
+		outBytes, err := TransformUsingFormat(out, format)
+
+		if err != nil {
+			writer.WriteHeader(http.StatusInternalServerError)
+			_, _ = fmt.Fprintf(writer, "Error '%s' transform: %s", format, err)
+			return
+		}
+
+		_, _ = writer.Write(outBytes)
+	})
+}
+
+func TransformUsingFormat(val interface{}, format string) ([]byte, error) {
+	var outBytes []byte
+	var err error
+
+	switch format {
+	case "yaml":
+		outBytes, err = yaml.Marshal(val)
+	case "text":
+		switch v := val.(type) {
+		case string:
+			outBytes = []byte(v)
+		case fmt.Stringer:
+			outBytes = []byte(v.String())
+		case []byte:
+			outBytes = v
+		}
+		if outBytes != nil {
+			break
+		}
+		fallthrough
+	case "json":
+		fallthrough
+	default:
+		outBytes, err = json.Marshal(val)
+	}
+
+	return outBytes, err
+}
diff --git a/pkg/hook/controller/hook_controller.go b/pkg/hook/controller/hook_controller.go
index 0daae6e..785ccb0 100644
--- a/pkg/hook/controller/hook_controller.go
+++ b/pkg/hook/controller/hook_controller.go
@@ -67,6 +67,8 @@ type HookController interface {
 
 	KubernetesSnapshots() map[string][]ObjectAndFilterResult
 	UpdateSnapshots([]BindingContext) []BindingContext
+	SnapshotsInfo() []string
+	SnapshotsDump() map[string]interface{}
 }
 
 var _ HookController = &hookController{}
@@ -337,3 +339,19 @@ func (hc *hookController) UpdateSnapshots(context []BindingContext) []BindingCon
 
 	return newContext
 }
+
+func (hc *hookController) SnapshotsInfo() []string {
+	if hc.KubernetesController == nil {
+		return []string{"no kubernetes bindings for hook"}
+	}
+
+	return hc.KubernetesController.SnapshotsInfo()
+}
+
+func (hc *hookController) SnapshotsDump() map[string]interface{} {
+	if hc.KubernetesController == nil {
+		return nil
+	}
+
+	return hc.KubernetesController.SnapshotsDump()
+}
diff --git a/pkg/hook/controller/kubernetes_bindings_controller.go b/pkg/hook/controller/kubernetes_bindings_controller.go
index 8eb3cf6..6c77e2a 100644
--- a/pkg/hook/controller/kubernetes_bindings_controller.go
+++ b/pkg/hook/controller/kubernetes_bindings_controller.go
@@ -36,6 +36,8 @@ type KubernetesBindingsController interface {
 	SnapshotsFrom(bindingNames ...string) map[string][]ObjectAndFilterResult
 	SnapshotsFor(bindingName string) []ObjectAndFilterResult
 	Snapshots() map[string][]ObjectAndFilterResult
+	SnapshotsInfo() []string
+	SnapshotsDump() map[string]interface{}
 }
 
 // kubernetesHooksController is a main implementation of KubernetesHooksController
@@ -278,6 +280,50 @@ func (c *kubernetesBindingsController) Snapshots() map[string][]ObjectAndFilterR
 	return c.SnapshotsFrom(c.BindingNames()...)
 }
 
+func (c *kubernetesBindingsController) SnapshotsInfo() []string {
+	infos := make([]string, 0)
+	for _, binding := range c.KubernetesBindings {
+		monitorID := binding.Monitor.Metadata.MonitorId
+		if c.kubeEventsManager.HasMonitor(monitorID) {
+			total, last := c.kubeEventsManager.GetMonitor(monitorID).SnapshotOperations()
+
+			info := fmt.Sprintf("%s: size=%d, operations since last execution: add=%d, mod=%d, del=%d, clear=%d, operations since start: add=%d, mod=%d, del=%d",
+				binding.BindingName,
+				total.Count,
+				last.Added,
+				last.Modified,
+				last.Deleted,
+				last.Cleaned,
+				total.Added,
+				total.Modified,
+				total.Deleted,
+			)
+			infos = append(infos, info)
+		}
+	}
+
+	return infos
+}
+
+func (c *kubernetesBindingsController) SnapshotsDump() map[string]interface{} {
+	dumps := make(map[string]interface{})
+	for _, binding := range c.KubernetesBindings {
+		monitorID := binding.Monitor.Metadata.MonitorId
+		if c.kubeEventsManager.HasMonitor(monitorID) {
+			total, last := c.kubeEventsManager.GetMonitor(monitorID).SnapshotOperations()
+			dumps[binding.BindingName] = map[string]interface{}{
+				"snapshot": c.kubeEventsManager.GetMonitor(monitorID).Snapshot(),
+				"operations": map[string]interface{}{
+					"sinceStart":         total,
+					"sinceLastExecution": last,
+				},
+			}
+		}
+	}
+
+	return dumps
+}
+
 func ConvertKubeEventToBindingContext(kubeEvent KubeEvent, link *KubernetesBindingToMonitorLink) []BindingContext {
 	bindingContexts := make([]BindingContext, 0)
 
diff --git a/pkg/kube_events_manager/monitor.go b/pkg/kube_events_manager/monitor.go
index b973c51..b5f5d35 100644
--- a/pkg/kube_events_manager/monitor.go
+++ b/pkg/kube_events_manager/monitor.go
@@ -26,6 +26,7 @@ type Monitor interface {
 	Snapshot() []ObjectAndFilterResult
 	EnableKubeEventCb()
 	GetConfig() *MonitorConfig
+	SnapshotOperations() (total *CachedObjectsInfo, last *CachedObjectsInfo)
 }
 
 // Monitor holds informers for resources and a namespace informer
@@ -217,8 +218,8 @@ func (m *monitor) Snapshot() []ObjectAndFilterResult {
 	return objects
 }
 
-// EnableKubeEventCb turns on event callback for each informer. This also calls
-// eventCb for events saved during "Synchronization" hook execution.
+// EnableKubeEventCb allows execution of event callback for all informers.
+// Also executes eventCb for events accumulated during "Synchronization" phase.
 func (m *monitor) EnableKubeEventCb() {
 	for _, informer := range m.ResourceInformers {
 		informer.EnableKubeEventCb()
@@ -312,3 +313,22 @@ func (m *monitor) PauseHandleEvents() {
 	}
 
 }
+
+func (m *monitor) SnapshotOperations() (total *CachedObjectsInfo, last *CachedObjectsInfo) {
+	total = &CachedObjectsInfo{}
+	last = &CachedObjectsInfo{}
+
+	for _, informer := range m.ResourceInformers {
+		total.Add(informer.CachedObjectsInfo())
+		last.Add(informer.CachedObjectsInfoIncrement())
+	}
+
+	for nsName := range m.VaryingInformers {
+		for _, informer := range m.VaryingInformers[nsName] {
+			total.Add(informer.CachedObjectsInfo())
+			last.Add(informer.CachedObjectsInfoIncrement())
+		}
+	}
+
+	return total, last
+}
diff --git a/pkg/kube_events_manager/resource_informer.go b/pkg/kube_events_manager/resource_informer.go
index 7b5ad72..fb13e99 100644
--- a/pkg/kube_events_manager/resource_informer.go
+++ b/pkg/kube_events_manager/resource_informer.go
@@ -35,6 +35,8 @@ type ResourceInformer interface {
 	Start()
 	Stop()
 	PauseHandleEvents()
+	CachedObjectsInfo() CachedObjectsInfo
+	CachedObjectsInfoIncrement() CachedObjectsInfo
 }
 
 type resourceInformer struct {
@@ -54,6 +56,11 @@ type resourceInformer struct {
 	cachedObjects map[string]*ObjectAndFilterResult
 	cacheLock     sync.RWMutex
 
+	// Cached objects operations since start
+	cachedObjectsInfo *CachedObjectsInfo
+	// Cached objects operations since last access
+	cachedObjectsIncrement *CachedObjectsInfo
+
 	// Events buffer for "Synchronization" mode: it stores events between CachedObjects call and EnableKubeEventCb call
 	// to replay them when "Synchronization" hook is done.
 	eventBuf     []KubeEvent
@@ -78,10 +85,12 @@ var _ ResourceInformer = &resourceInformer{}
 
 var NewResourceInformer = func(monitor *MonitorConfig) ResourceInformer {
 	informer := &resourceInformer{
-		Monitor:       monitor,
-		cachedObjects: make(map[string]*ObjectAndFilterResult),
-		cacheLock:     sync.RWMutex{},
-		eventBufLock:  sync.Mutex{},
+		Monitor:                monitor,
+		cachedObjects:          make(map[string]*ObjectAndFilterResult),
+		cacheLock:              sync.RWMutex{},
+		eventBufLock:           sync.Mutex{},
+		cachedObjectsInfo:      &CachedObjectsInfo{},
+		cachedObjectsIncrement: &CachedObjectsInfo{},
 	}
 	return informer
 }
@@ -267,6 +276,7 @@ func (ei *resourceInformer) LoadExistedObjects() error {
 		ei.cachedObjects[k] = v
 	}
 
+	ei.cachedObjectsInfo.Count = uint64(len(ei.cachedObjects))
 	ei.metricStorage.GaugeSet("{PREFIX}kube_snapshot_objects", float64(len(ei.cachedObjects)), ei.Monitor.Metadata.MetricLabels)
 	ei.metricStorage.GaugeSet("{PREFIX}kube_snapshot_bytes", float64(ObjectAndFilterResults(ei.cachedObjects).Bytes()), ei.Monitor.Metadata.MetricLabels)
 
@@ -349,6 +359,16 @@ func (ei *resourceInformer) HandleWatchEvent(object interface{}, eventType Watch
 			skipEvent = true
 		}
 		ei.cachedObjects[resourceId] = objFilterRes
+		// Update cached objects info.
+		ei.cachedObjectsInfo.Count = uint64(len(ei.cachedObjects))
+		if eventType == WatchEventAdded {
+			ei.cachedObjectsInfo.Added++
+			ei.cachedObjectsIncrement.Added++
+		} else {
+			ei.cachedObjectsInfo.Modified++
+			ei.cachedObjectsIncrement.Modified++
+		}
+		// Update metrics.
 		ei.metricStorage.GaugeSet("{PREFIX}kube_snapshot_objects", float64(len(ei.cachedObjects)), ei.Monitor.Metadata.MetricLabels)
 		ei.metricStorage.GaugeSet("{PREFIX}kube_snapshot_bytes", float64(ObjectAndFilterResults(ei.cachedObjects).Bytes()), ei.Monitor.Metadata.MetricLabels)
 		ei.cacheLock.Unlock()
@@ -359,6 +379,15 @@ func (ei *resourceInformer) HandleWatchEvent(object interface{}, eventType Watch
 	case WatchEventDeleted:
 		ei.cacheLock.Lock()
 		delete(ei.cachedObjects, resourceId)
+		// Update cached objects info.
+		ei.cachedObjectsInfo.Count = uint64(len(ei.cachedObjects))
+		if ei.cachedObjectsInfo.Count == 0 {
+			ei.cachedObjectsInfo.Cleaned++
+			ei.cachedObjectsIncrement.Cleaned++
+		}
+		ei.cachedObjectsInfo.Deleted++
+		ei.cachedObjectsIncrement.Deleted++
+		// Update metrics.
 		ei.metricStorage.GaugeSet("{PREFIX}kube_snapshot_objects", float64(len(ei.cachedObjects)), ei.Monitor.Metadata.MetricLabels)
 		ei.metricStorage.GaugeSet("{PREFIX}kube_snapshot_bytes", float64(ObjectAndFilterResults(ei.cachedObjects).Bytes()), ei.Monitor.Metadata.MetricLabels)
 		ei.cacheLock.Unlock()
@@ -470,3 +499,19 @@ func (ei *resourceInformer) PauseHandleEvents() {
 	log.Debugf("%s: PAUSE resource informer", ei.Monitor.Metadata.DebugName)
 	ei.stopped = true
 }
+
+// CachedObjectsInfo returns info accumulated from start.
+func (ei *resourceInformer) CachedObjectsInfo() CachedObjectsInfo {
+	ei.cacheLock.RLock()
+	defer ei.cacheLock.RUnlock()
+	return *ei.cachedObjectsInfo
+}
+
+// CachedObjectsInfoIncrement returns info accumulated from last call and clean it.
+func (ei *resourceInformer) CachedObjectsInfoIncrement() CachedObjectsInfo {
+	ei.cacheLock.Lock()
+	defer ei.cacheLock.Unlock()
+	info := *ei.cachedObjectsIncrement
+	ei.cachedObjectsIncrement = &CachedObjectsInfo{}
+	return info
+}
diff --git a/pkg/kube_events_manager/types/types.go b/pkg/kube_events_manager/types/types.go
index a4d6381..490116a 100644
--- a/pkg/kube_events_manager/types/types.go
+++ b/pkg/kube_events_manager/types/types.go
@@ -47,6 +47,7 @@ type ObjectAndFilterResult struct {
 	ObjectSize       int
 }
 
+// Map constructs a map suitable for use in binding context.
 func (o ObjectAndFilterResult) Map() map[string]interface{} {
 	m := map[string]interface{}{}
 
@@ -54,29 +55,38 @@ func (o ObjectAndFilterResult) Map() map[string]interface{} {
 		m["object"] = o.Object
 	}
 
-	if o.Metadata.JqFilter == "" {
-		return m
-	}
-	// Add filterResult field only if it was requested
-	filterResString, ok := o.FilterResult.(string)
-	if !ok || filterResString == "" {
-		m["filterResult"] = nil
+	if o.Metadata.JqFilter == "" && o.FilterResult == nil {
+		// No jqFilter, no filterResult -> filterResult field should not be in a map.
 		return m
 	}
 
-	if filterResString == "" {
-		m["filterResult"] = nil
-		return m
-	}
+	var filterResultValue interface{}
+	if o.Metadata.JqFilter != "" {
+		// jqFilter is set, so filterResult field should be in a map.
+		// FilterResult is a jq output and should be a string.
+		filterResString, ok := o.FilterResult.(string)
+		if !ok || filterResString == "" {
+			m["filterResult"] = nil
+			return m
+		}
 
-	var res interface{}
-	err := json.Unmarshal([]byte(filterResString), &res)
-	if err != nil {
-		log.Errorf("Possible bug!!! Cannot unmarshal jq filter '%s' result: %s", o.Metadata.JqFilter, err)
-		m["filterResult"] = nil
-		return m
+		if filterResString == "" {
+			m["filterResult"] = nil
+			return m
+		}
+
+		// Convert string with jq output into Go object.
+		err := json.Unmarshal([]byte(filterResString), &filterResultValue)
+		if err != nil {
+			log.Errorf("Possible bug!!! Cannot unmarshal jq filter '%s' result: %s", o.Metadata.JqFilter, err)
+			m["filterResult"] = nil
+			return m
+		}
+	} else {
+		filterResultValue = o.FilterResult
 	}
-	m["filterResult"] = res
+
+	m["filterResult"] = filterResultValue
 
 	return m
 }
diff --git a/pkg/kube_events_manager/util.go b/pkg/kube_events_manager/util.go
index 555cb9f..e100a40 100644
--- a/pkg/kube_events_manager/util.go
+++ b/pkg/kube_events_manager/util.go
@@ -63,3 +63,20 @@ func RandomizedResyncPeriod() time.Duration {
 
 	return ResyncPeriodMedian - (ResyncPeriodSpread / 2) + rndSpreadDelta + rndJitterDelta
 }
+
+// CachedObjectsInfo stores counters of operations over resources in Monitors and Informers.
+type CachedObjectsInfo struct {
+	Count    uint64 `json:"count"`
+	Added    uint64 `json:"added"`
+	Deleted  uint64 `json:"deleted"`
+	Modified uint64 `json:"modified"`
+	Cleaned  uint64 `json:"cleaned"`
+}
+
+func (c *CachedObjectsInfo) Add(in CachedObjectsInfo) {
+	c.Count += in.Count
+	c.Added += in.Added
+	c.Deleted += in.Deleted
+	c.Modified += in.Modified
+	c.Cleaned += in.Cleaned
+}
diff --git a/pkg/shell-operator/operator.go b/pkg/shell-operator/operator.go
index b6e9a27..262467d 100644
--- a/pkg/shell-operator/operator.go
+++ b/pkg/shell-operator/operator.go
@@ -18,7 +18,6 @@ import (
 	. "github.com/flant/shell-operator/pkg/hook/types"
 	"github.com/flant/shell-operator/pkg/kube/object_patch"
 	. "github.com/flant/shell-operator/pkg/kube_events_manager/types"
-	structured_logger "github.com/flant/shell-operator/pkg/utils/structured-logger"
 	. "github.com/flant/shell-operator/pkg/webhook/validating/types"
 
 	klient "github.com/flant/kube-client/client"
@@ -740,6 +739,10 @@ func (op *ShellOperator) TaskHandleHookRun(t task.Task) queue.TaskResult {
 }
 
 func (op *ShellOperator) HandleRunHook(t task.Task, taskHook *hook.Hook, hookMeta HookMetadata, taskLogEntry *log.Entry, hookLogLabels map[string]string, metricLabels map[string]string) error {
+	for _, info := range taskHook.HookController.SnapshotsInfo() {
+		taskLogEntry.Infof("snapshot info: %s", info)
+	}
+
 	result, err := taskHook.Run(hookMeta.BindingType, hookMeta.BindingContext, hookLogLabels)
 	if err != nil {
 		return err
@@ -1013,14 +1016,26 @@ func (op *ShellOperator) RunMetrics() {
 }
 
 func (op *ShellOperator) SetupDebugServerHandles() {
-	op.DebugServer.Router.Get("/", func(writer http.ResponseWriter, request *http.Request) {
-		_, _ = fmt.Fprintf(writer, "%s control endpoint is alive", app.AppName)
+	op.DebugServer.Route("/", func(_ *http.Request) (interface{}, error) {
+		return fmt.Sprintf("%s control endpoint is alive", app.AppName), nil
 	})
 
-	op.DebugServer.Router.Get("/queue/list.{format:(json|yaml|text)}", func(writer http.ResponseWriter, request *http.Request) {
-		format := chi.URLParam(request, "format")
-		structured_logger.GetLogEntry(request).Debugf("queue list using format %s", format)
-		_, _ = writer.Write([]byte(dump.TaskQueueSetToText(op.TaskQueues)))
+	op.DebugServer.Route("/queue/main.{format:(json|yaml|text)}", func(_ *http.Request) (interface{}, error) {
+		return dump.TaskQueueMainToText(op.TaskQueues), nil
+	})
+
+	op.DebugServer.Route("/queue/list.{format:(json|yaml|text)}", func(_ *http.Request) (interface{}, error) {
+		return dump.TaskQueueSetToText(op.TaskQueues), nil
+	})
+
+	op.DebugServer.Route("/hook/list.{format:(json|yaml|text)}", func(_ *http.Request) (interface{}, error) {
+		return op.HookManager.GetHookNames(), nil
+	})
+
+	op.DebugServer.Route("/hook/{name}/snapshots.{format:(json|yaml|text)}", func(r *http.Request) (interface{}, error) {
+		hookName := chi.URLParam(r, "name")
+		h := op.HookManager.GetHook(hookName)
+		return h.HookController.SnapshotsDump(), nil
 	})
 }
 
diff --git a/pkg/task/dump/dump.go b/pkg/task/dump/dump.go
index 7738ca1..5058814 100644
--- a/pkg/task/dump/dump.go
+++ b/pkg/task/dump/dump.go
@@ -28,6 +28,19 @@ func (a AsQueueNames) Less(i, j int) bool {
 	return p < q
 }
 
+func TaskQueueMainToText(tqs *queue.TaskQueueSet) string {
+	var buf strings.Builder
+
+	q := tqs.GetMain()
+	if q == nil {
+		buf.WriteString("Queue 'main' is not created\n")
+	} else {
+		buf.WriteString(TaskQueueToText(q))
+	}
+
+	return buf.String()
+}
+
 func TaskQueueSetToText(tqs *queue.TaskQueueSet) string {
 	var buf strings.Builder
 


Test patch:
diff --git a/pkg/hook/binding_context/binding_context_test.go b/pkg/hook/binding_context/binding_context_test.go
index 3187bcb..934f733 100644
--- a/pkg/hook/binding_context/binding_context_test.go
+++ b/pkg/hook/binding_context/binding_context_test.go
@@ -104,7 +104,6 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 				}
 				bc.Metadata.BindingType = OnKubernetesEvent
 
-				// object without jqfilter should not have filterResult field
 				obj := ObjectAndFilterResult{
 					Object: &unstructured.Unstructured{
 						Object: map[string]interface{}{
@@ -115,7 +114,6 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 							"kind": "Pod",
 						},
 					},
-					FilterResult: "asd",
 				}
 				obj.Metadata.JqFilter = ""
 				bc.Objects = append(bc.Objects, obj)
@@ -188,7 +186,7 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 				bc.Metadata.BindingType = OnKubernetesEvent
 				bc.Metadata.Group = "pods"
 				bc.Metadata.IncludeSnapshots = []string{"monitor_pods"}
-				// object without jqfilter should not have filterResult field
+
 				obj := ObjectAndFilterResult{
 					Object: &unstructured.Unstructured{
 						Object: map[string]interface{}{
@@ -199,7 +197,6 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 							"kind": "Pod",
 						},
 					},
-					FilterResult: "asd",
 				}
 				bc.Objects = append(bc.Objects, obj)
 				bc.Snapshots["monitor_pods"] = append(bc.Snapshots["monitor_pods"], obj)
@@ -212,7 +209,7 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 					Objects:    []ObjectAndFilterResult{},
 				}
 				bc.Metadata.BindingType = OnKubernetesEvent
-				// object without jqfilter should not have filterResult field
+
 				obj = ObjectAndFilterResult{
 					Object: &unstructured.Unstructured{
 						Object: map[string]interface{}{
@@ -223,7 +220,6 @@ func Test_ConvertBindingContextList_v1(t *testing.T) {
 							"kind": "Pod",
 						},
 					},
-					FilterResult: "asd",
 				}
 				bc.Objects = append(bc.Objects, obj)
 
diff --git a/pkg/kube_events_manager/types/types_test.go b/pkg/kube_events_manager/types/types_test.go
index bf9f7cf..81717e2 100644
--- a/pkg/kube_events_manager/types/types_test.go
+++ b/pkg/kube_events_manager/types/types_test.go
@@ -57,7 +57,7 @@ func Test_ObjectAndFilterResult_ToJson_NullFilterResult(t *testing.T) {
 	assert.Contains(t, jsonStr, `"filterResult":null`)
 }
 
-func Test_ObjectAndFilterResult_ToJson_NoFilterResult(t *testing.T) {
+func Test_ObjectAndFilterResult_ToJson_Empty_JQFilter_Has_FilterResult(t *testing.T) {
 	obj := newObj("Pod", "default", "pod-qwe", `{"spec":"asd"}`)
 	obj.Metadata.JqFilter = ""
 
@@ -65,7 +65,7 @@ func Test_ObjectAndFilterResult_ToJson_NoFilterResult(t *testing.T) {
 	assert.NoError(t, err)
 	jsonStr := string(data)
 	//fmt.Printf("%s\n", jsonStr)
-	assert.NotContains(t, jsonStr, "filterResult")
+	assert.Contains(t, jsonStr, "filterResult")
 }
 
 func Test_Sort_ByNamespaceAndName(t *testing.T) {