github.com/mackerelio/mackerel-agent-plugins@v0.89.3/mackerel-plugin-sidekiq/lib/sidekiq.go (about)

     1  package mpsidekiq
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"flag"
     7  	"fmt"
     8  	"os"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	mp "github.com/mackerelio/go-mackerel-plugin-helper"
    14  	r "github.com/redis/go-redis/v9"
    15  )
    16  
    17  // SidekiqPlugin for fetching metrics
    18  type SidekiqPlugin struct {
    19  	Client    *r.Client
    20  	Namespace string
    21  	Prefix    string
    22  }
    23  
    24  var graphdef = map[string]mp.Graphs{
    25  	"ProcessedANDFailed": {
    26  		Label: "Sidekiq processed and failed count",
    27  		Unit:  "integer",
    28  		Metrics: []mp.Metrics{
    29  			{Name: "processed", Label: "Processed", Type: "uint64", Diff: true},
    30  			{Name: "failed", Label: "Failed", Type: "uint64", Diff: true},
    31  		},
    32  	},
    33  	"Stats": {
    34  		Label: "Sidekiq stats",
    35  		Unit:  "integer",
    36  		Metrics: []mp.Metrics{
    37  			{Name: "busy", Label: "Busy", Type: "uint64"},
    38  			{Name: "enqueued", Label: "Enqueued", Type: "uint64"},
    39  			{Name: "schedule", Label: "Schedule", Type: "uint64"},
    40  			{Name: "retry", Label: "Retry", Type: "uint64"},
    41  			{Name: "dead", Label: "Dead", Type: "uint64"},
    42  		},
    43  	},
    44  	"QueueLatency": {
    45  		Label: "Sidekiq queue latency",
    46  		Unit:  "float",
    47  		Metrics: []mp.Metrics{
    48  			{Name: "*", Label: "%1"},
    49  		},
    50  	},
    51  }
    52  
    53  // GraphDefinition Graph definition
    54  func (sp SidekiqPlugin) GraphDefinition() map[string]mp.Graphs {
    55  	return graphdef
    56  }
    57  
    58  func (sp SidekiqPlugin) get(ctx context.Context, key string) uint64 {
    59  	val, err := sp.Client.Get(ctx, key).Result()
    60  	if err == r.Nil {
    61  		return 0
    62  	}
    63  
    64  	r, _ := strconv.ParseUint(val, 10, 64)
    65  	return r
    66  }
    67  
    68  func (sp SidekiqPlugin) zCard(ctx context.Context, key string) uint64 {
    69  	val, err := sp.Client.ZCard(ctx, key).Result()
    70  	if err == r.Nil {
    71  		return 0
    72  	}
    73  
    74  	return uint64(val)
    75  }
    76  
    77  func (sp SidekiqPlugin) sMembers(ctx context.Context, key string) []string {
    78  	val, err := sp.Client.SMembers(ctx, key).Result()
    79  	if err == r.Nil {
    80  		return make([]string, 0)
    81  	}
    82  
    83  	return val
    84  }
    85  
    86  func (sp SidekiqPlugin) hGet(ctx context.Context, key string, field string) uint64 {
    87  	val, err := sp.Client.HGet(ctx, key, field).Result()
    88  	if err == r.Nil {
    89  		return 0
    90  	}
    91  
    92  	r, _ := strconv.ParseUint(val, 10, 64)
    93  	return r
    94  }
    95  
    96  func (sp SidekiqPlugin) lLen(ctx context.Context, key string) uint64 {
    97  	val, err := sp.Client.LLen(ctx, key).Result()
    98  	if err == r.Nil {
    99  		return 0
   100  	}
   101  
   102  	return uint64(val)
   103  }
   104  
   105  func addNamespace(namespace string, key string) string {
   106  	if namespace == "" {
   107  		return key
   108  	}
   109  	return namespace + ":" + key
   110  }
   111  
   112  func (sp SidekiqPlugin) getProcessed(ctx context.Context) uint64 {
   113  	key := addNamespace(sp.Namespace, "stat:processed")
   114  	return sp.get(ctx, key)
   115  }
   116  
   117  func (sp SidekiqPlugin) getFailed(ctx context.Context) uint64 {
   118  	key := addNamespace(sp.Namespace, "stat:failed")
   119  	return sp.get(ctx, key)
   120  }
   121  
   122  func inject(slice []uint64, base uint64) uint64 {
   123  	for _, e := range slice {
   124  		base += uint64(e)
   125  	}
   126  
   127  	return base
   128  }
   129  
   130  func (sp SidekiqPlugin) getBusy(ctx context.Context) uint64 {
   131  	key := addNamespace(sp.Namespace, "processes")
   132  	processes := sp.sMembers(ctx, key)
   133  	busies := make([]uint64, 10)
   134  	for _, e := range processes {
   135  		e := addNamespace(sp.Namespace, e)
   136  		busies = append(busies, sp.hGet(ctx, e, "busy"))
   137  	}
   138  
   139  	return inject(busies, 0)
   140  }
   141  
   142  func (sp SidekiqPlugin) getEnqueued(ctx context.Context) uint64 {
   143  	key := addNamespace(sp.Namespace, "queues")
   144  	queues := sp.sMembers(ctx, key)
   145  	queuesLlens := make([]uint64, 10)
   146  
   147  	prefix := addNamespace(sp.Namespace, "queue:")
   148  	for _, e := range queues {
   149  		queuesLlens = append(queuesLlens, sp.lLen(ctx, prefix+e))
   150  	}
   151  
   152  	return inject(queuesLlens, 0)
   153  }
   154  
   155  func (sp SidekiqPlugin) getSchedule(ctx context.Context) uint64 {
   156  	key := addNamespace(sp.Namespace, "schedule")
   157  	return sp.zCard(ctx, key)
   158  }
   159  
   160  func (sp SidekiqPlugin) getRetry(ctx context.Context) uint64 {
   161  	key := addNamespace(sp.Namespace, "retry")
   162  	return sp.zCard(ctx, key)
   163  }
   164  
   165  func (sp SidekiqPlugin) getDead(ctx context.Context) uint64 {
   166  	key := addNamespace(sp.Namespace, "dead")
   167  	return sp.zCard(ctx, key)
   168  }
   169  
   170  func (sp SidekiqPlugin) getProcessedFailed(ctx context.Context) map[string]interface{} {
   171  	data := make(map[string]interface{}, 20)
   172  
   173  	data["processed"] = sp.getProcessed(ctx)
   174  	data["failed"] = sp.getFailed(ctx)
   175  
   176  	return data
   177  }
   178  
   179  func (sp SidekiqPlugin) getStats(ctx context.Context, field []string) map[string]interface{} {
   180  	stats := make(map[string]interface{}, 20)
   181  	for _, e := range field {
   182  		switch e {
   183  		case "busy":
   184  			stats[e] = sp.getBusy(ctx)
   185  		case "enqueued":
   186  			stats[e] = sp.getEnqueued(ctx)
   187  		case "schedule":
   188  			stats[e] = sp.getSchedule(ctx)
   189  		case "retry":
   190  			stats[e] = sp.getRetry(ctx)
   191  		case "dead":
   192  			stats[e] = sp.getDead(ctx)
   193  		}
   194  	}
   195  
   196  	return stats
   197  }
   198  
   199  func metricName(names ...string) string {
   200  	return strings.Join(names, ".")
   201  }
   202  
   203  func (sp SidekiqPlugin) getQueueLatency(ctx context.Context) map[string]interface{} {
   204  	latency := make(map[string]interface{}, 10)
   205  
   206  	key := addNamespace(sp.Namespace, "queues")
   207  	queues := sp.sMembers(ctx, key)
   208  
   209  	prefix := addNamespace(sp.Namespace, "queue:")
   210  	for _, q := range queues {
   211  		queuesLRange, err := sp.Client.LRange(ctx, prefix+q, -1, -1).Result()
   212  		if err != nil {
   213  			fmt.Fprintf(os.Stderr, "get last queue error")
   214  		}
   215  
   216  		if len(queuesLRange) == 0 {
   217  			latency[metricName("QueueLatency", q)] = 0.0
   218  			continue
   219  		}
   220  		var job map[string]interface{}
   221  		var thence float64
   222  
   223  		err = json.Unmarshal([]byte(queuesLRange[0]), &job)
   224  		if err != nil {
   225  			fmt.Fprintf(os.Stderr, "json parse error")
   226  			continue
   227  		}
   228  		now := float64(time.Now().Unix())
   229  		if enqueuedAt, ok := job["enqueued_at"]; ok {
   230  			enqueuedAt := enqueuedAt.(float64)
   231  			thence = enqueuedAt
   232  		} else {
   233  			thence = now
   234  		}
   235  		latency[metricName("QueueLatency", q)] = (now - thence)
   236  	}
   237  
   238  	return latency
   239  }
   240  
   241  // FetchMetrics fetch the metrics
   242  func (sp SidekiqPlugin) FetchMetrics() (map[string]interface{}, error) {
   243  	field := []string{"busy", "enqueued", "schedule", "retry", "dead", "latency"}
   244  	ctx := context.Background()
   245  	stats := sp.getStats(ctx, field)
   246  	pf := sp.getProcessedFailed(ctx)
   247  	latency := sp.getQueueLatency(ctx)
   248  
   249  	// merge maps
   250  	m := func(m ...map[string]interface{}) map[string]interface{} {
   251  		r := make(map[string]interface{}, 20)
   252  		for _, c := range m {
   253  			for k, v := range c {
   254  				r[k] = v
   255  			}
   256  		}
   257  
   258  		return r
   259  	}(stats, pf, latency)
   260  
   261  	return m, nil
   262  }
   263  
   264  // MetricKeyPrefix interface for PluginWithPrefix
   265  func (sp SidekiqPlugin) MetricKeyPrefix() string {
   266  	if sp.Prefix == "" {
   267  		sp.Prefix = "sidekiq"
   268  	}
   269  	return sp.Prefix
   270  }
   271  
   272  // Do the plugin
   273  func Do() {
   274  	optHost := flag.String("host", "localhost", "Hostname")
   275  	optPort := flag.String("port", "6379", "Port")
   276  	optPassword := flag.String("password", os.Getenv("SIDEKIQ_PASSWORD"), "Password")
   277  	optDB := flag.Int("db", 0, "DB")
   278  	optNamespace := flag.String("redis-namespace", "", "Redis namespace")
   279  	optPrefix := flag.String("metric-key-prefix", "sidekiq", "Metric key prefix")
   280  	optTempfile := flag.String("tempfile", "", "Temp file name")
   281  	flag.Parse()
   282  
   283  	client := r.NewClient(&r.Options{
   284  		Addr:     fmt.Sprintf("%s:%s", *optHost, *optPort),
   285  		Password: *optPassword,
   286  		DB:       *optDB,
   287  	})
   288  
   289  	sp := SidekiqPlugin{
   290  		Client:    client,
   291  		Namespace: *optNamespace,
   292  		Prefix:    *optPrefix,
   293  	}
   294  	helper := mp.NewMackerelPlugin(sp)
   295  	helper.Tempfile = *optTempfile
   296  
   297  	helper.Run()
   298  }