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 }