github.com/mackerelio/mackerel-agent-plugins@v0.89.3/mackerel-plugin-fluentd/lib/fluentd.go (about) 1 package mpfluentd 2 3 import ( 4 "crypto/md5" 5 "encoding/json" 6 "flag" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "regexp" 12 "strconv" 13 "strings" 14 15 mp "github.com/mackerelio/go-mackerel-plugin-helper" 16 "golang.org/x/text/cases" 17 "golang.org/x/text/language" 18 ) 19 20 func metricName(names ...string) string { 21 return strings.Join(names, ".") 22 } 23 24 // FluentdPlugin mackerel plugin for Fluentd 25 type FluentdPlugin struct { 26 Host string 27 Port string 28 Prefix string 29 Tempfile string 30 pluginType string 31 pluginIDPattern *regexp.Regexp 32 extendedMetrics []string 33 Workers uint 34 35 plugins []FluentdPluginMetrics 36 } 37 38 // FluentdMetrics is alias for backward compatibility. 39 type FluentdMetrics = FluentdPlugin 40 41 // MetricKeyPrefix interface for PluginWithPrefix 42 func (f FluentdPlugin) MetricKeyPrefix() string { 43 if f.Prefix == "" { 44 f.Prefix = "fluentd" 45 } 46 return f.Prefix 47 } 48 49 // FluentdPluginMetrics metrics 50 type FluentdPluginMetrics struct { 51 RetryCount uint64 `json:"retry_count"` 52 BufferQueueLength uint64 `json:"buffer_queue_length"` 53 BufferTotalQueuedSize uint64 `json:"buffer_total_queued_size"` 54 OutputPlugin bool `json:"output_plugin"` 55 Type string `json:"type"` 56 PluginCategory string `json:"plugin_category"` 57 PluginID string `json:"plugin_id"` 58 normalizedPluginID string 59 60 // extended metrics fluentd >= 1.6 61 // https://www.fluentd.org/blog/fluentd-v1.6.0-has-been-released 62 EmitRecords uint64 `json:"emit_records"` 63 EmitCount uint64 `json:"emit_count"` 64 WriteCount uint64 `json:"write_count"` 65 RollbackCount uint64 `json:"rollback_count"` 66 SlowFlushCount uint64 `json:"slow_flush_count"` 67 FlushTimeCount uint64 `json:"flush_time_count"` 68 BufferStageLength uint64 `json:"buffer_stage_length"` 69 BufferStageByteSize uint64 `json:"buffer_stage_byte_size"` 70 BufferQueueByteSize uint64 `json:"buffer_queue_byte_size"` 71 BufferAvailableBufferSpaceRatios float64 `json:"buffer_available_buffer_space_ratios"` 72 } 73 74 func (fpm FluentdPluginMetrics) getExtended(name string) float64 { 75 switch name { 76 case "emit_records": 77 return float64(fpm.EmitRecords) 78 case "emit_count": 79 return float64(fpm.EmitCount) 80 case "write_count": 81 return float64(fpm.WriteCount) 82 case "rollback_count": 83 return float64(fpm.RollbackCount) 84 case "slow_flush_count": 85 return float64(fpm.SlowFlushCount) 86 case "flush_time_count": 87 return float64(fpm.FlushTimeCount) 88 case "buffer_stage_length": 89 return float64(fpm.BufferStageLength) 90 case "buffer_stage_byte_size": 91 return float64(fpm.BufferStageByteSize) 92 case "buffer_queue_byte_size": 93 return float64(fpm.BufferQueueByteSize) 94 case "buffer_available_buffer_space_ratios": 95 return fpm.BufferAvailableBufferSpaceRatios 96 } 97 return 0 98 } 99 100 // FluentMonitorJSON monitor json 101 type FluentMonitorJSON struct { 102 Plugins []FluentdPluginMetrics `json:"plugins"` 103 } 104 105 var normalizePluginIDRe = regexp.MustCompile(`[^-a-zA-Z0-9_]`) 106 107 func normalizePluginID(in string) string { 108 return normalizePluginIDRe.ReplaceAllString(in, "_") 109 } 110 111 func (fpm FluentdPluginMetrics) getNormalizedPluginID() string { 112 if fpm.normalizedPluginID == "" { 113 fpm.normalizedPluginID = normalizePluginID(fpm.PluginID) 114 } 115 return fpm.normalizedPluginID 116 } 117 118 func (f *FluentdPlugin) parseStats(body []byte) (map[string]interface{}, error) { 119 var j FluentMonitorJSON 120 err := json.Unmarshal(body, &j) 121 f.plugins = j.Plugins 122 123 metrics := make(map[string]interface{}) 124 for _, p := range f.plugins { 125 if f.nonTargetPlugin(p) { 126 continue 127 } 128 pid := p.getNormalizedPluginID() 129 metrics[metricName("retry_count", pid)] = float64(p.RetryCount) 130 metrics[metricName("buffer_queue_length", pid)] = float64(p.BufferQueueLength) 131 metrics[metricName("buffer_total_queued_size", pid)] = float64(p.BufferTotalQueuedSize) 132 for _, name := range f.extendedMetrics { 133 metrics[metricName(name, pid)] = p.getExtended(name) 134 } 135 } 136 return metrics, err 137 } 138 139 func (f *FluentdPlugin) nonTargetPlugin(plugin FluentdPluginMetrics) bool { 140 if plugin.PluginCategory != "output" { 141 return true 142 } 143 if f.pluginType != "" && f.pluginType != plugin.Type { 144 return true 145 } 146 if f.pluginIDPattern != nil && !f.pluginIDPattern.MatchString(plugin.PluginID) { 147 return true 148 } 149 return false 150 } 151 152 func (f *FluentdPlugin) fetchFluentdMetrics(host string, port int) (map[string]interface{}, error) { 153 target := fmt.Sprintf("http://%s:%d/api/plugins.json", host, port) 154 req, err := http.NewRequest(http.MethodGet, target, nil) 155 if err != nil { 156 return nil, err 157 } 158 req.Header.Set("User-Agent", "mackerel-plugin-fluentd") 159 160 resp, err := http.DefaultClient.Do(req) 161 if err != nil { 162 return nil, err 163 } 164 defer resp.Body.Close() 165 body, err := io.ReadAll(resp.Body) 166 if err != nil { 167 return nil, err 168 } 169 170 return f.parseStats(body) 171 } 172 173 // FetchMetrics interface for mackerelplugin 174 func (f FluentdPlugin) FetchMetrics() (map[string]interface{}, error) { 175 port, err := strconv.Atoi(f.Port) 176 if err != nil { 177 return nil, err 178 } 179 if f.Workers > 1 { 180 metrics := make(map[string]interface{}) 181 for workerNumber := 0; workerNumber < int(f.Workers); workerNumber++ { 182 m, e := f.fetchFluentdMetrics(f.Host, port+workerNumber) 183 if e != nil { 184 continue 185 } 186 187 workerName := fmt.Sprintf("worker%d", workerNumber) 188 for k, v := range m { 189 ks := strings.Split(k, ".") 190 ks, last := ks[:len(ks)-1], ks[len(ks)-1] 191 ks = append(ks, workerName) 192 ks = append(ks, last) 193 metrics[strings.Join(ks, ".")] = v 194 } 195 } 196 if len(metrics) == 0 { 197 err := fmt.Errorf("failed to connect to fluentd's monitor_agent") 198 return metrics, err 199 } 200 return metrics, nil 201 } 202 return f.fetchFluentdMetrics(f.Host, port) 203 } 204 205 var defaultGraphs = map[string]mp.Graphs{ 206 "retry_count": { 207 Label: "retry count", 208 Unit: "integer", 209 Metrics: []mp.Metrics{ 210 {Name: "*", Label: "%1", Diff: false}, 211 }, 212 }, 213 "buffer_queue_length": { 214 Label: "queue length", 215 Unit: "integer", 216 Metrics: []mp.Metrics{ 217 {Name: "*", Label: "%1", Diff: false}, 218 }, 219 }, 220 "buffer_total_queued_size": { 221 Label: "buffer total queued size", 222 Unit: "integer", 223 Metrics: []mp.Metrics{ 224 {Name: "*", Label: "%1", Diff: false}, 225 }, 226 }, 227 } 228 229 var extendedGraphs = map[string]mp.Graphs{ 230 "emit_records": { 231 Label: "emitted records", 232 Unit: "integer", 233 Metrics: []mp.Metrics{ 234 {Name: "*", Label: "%1", Diff: true}, 235 }, 236 }, 237 "emit_count": { 238 Label: "emit calls", 239 Unit: "integer", 240 Metrics: []mp.Metrics{ 241 {Name: "*", Label: "%1", Diff: true}, 242 }, 243 }, 244 "write_count": { 245 Label: "write/try_write calls", 246 Unit: "integer", 247 Metrics: []mp.Metrics{ 248 {Name: "*", Label: "%1", Diff: true}, 249 }, 250 }, 251 "rollback_count": { 252 Label: "rollbacks", 253 Unit: "integer", 254 Metrics: []mp.Metrics{ 255 {Name: "*", Label: "%1", Diff: true}, 256 }, 257 }, 258 "slow_flush_count": { 259 Label: "slow flushes", 260 Unit: "integer", 261 Metrics: []mp.Metrics{ 262 {Name: "*", Label: "%1", Diff: true}, 263 }, 264 }, 265 "flush_time_count": { 266 Label: "buffer flush time in msec", 267 Unit: "integer", 268 Metrics: []mp.Metrics{ 269 {Name: "*", Label: "%1", Diff: true}, 270 }, 271 }, 272 "buffer_stage_length": { 273 Label: "length of staged buffer chunks", 274 Unit: "integer", 275 Metrics: []mp.Metrics{ 276 {Name: "*", Label: "%1", Diff: false}, 277 }, 278 }, 279 "buffer_stage_byte_size": { 280 Label: "bytesize of staged buffer chunks", 281 Unit: "integer", 282 Metrics: []mp.Metrics{ 283 {Name: "*", Label: "%1", Diff: false}, 284 }, 285 }, 286 "buffer_queue_byte_size": { 287 Label: "bytesize of queued buffer chunks", 288 Unit: "integer", 289 Metrics: []mp.Metrics{ 290 {Name: "*", Label: "%1", Diff: false}, 291 }, 292 }, 293 "buffer_available_buffer_space_ratios": { 294 Label: "available space for buffer", 295 Unit: "percentage", 296 Metrics: []mp.Metrics{ 297 {Name: "*", Label: "%1", Diff: false}, 298 }, 299 }, 300 } 301 302 // GraphDefinition interface for mackerelplugin 303 func (f FluentdPlugin) GraphDefinition() map[string]mp.Graphs { 304 labelPrefix := cases.Title(language.Und, cases.NoLower).String(f.Prefix) 305 graphs := make(map[string]mp.Graphs, len(defaultGraphs)) 306 for key, g := range defaultGraphs { 307 if f.Workers > 1 { 308 key = metricName(key, "#") 309 } 310 graphs[key] = mp.Graphs{ 311 Label: (labelPrefix + " " + g.Label), 312 Unit: g.Unit, 313 Metrics: g.Metrics, 314 } 315 } 316 for _, name := range f.extendedMetrics { 317 if g, ok := extendedGraphs[name]; ok { 318 if f.Workers > 1 { 319 name = metricName(name, "#") 320 } 321 graphs[name] = mp.Graphs{ 322 Label: (labelPrefix + " " + g.Label), 323 Unit: g.Unit, 324 Metrics: g.Metrics, 325 } 326 } 327 } 328 return graphs 329 } 330 331 // Do the plugin 332 func Do() { 333 host := flag.String("host", "localhost", "fluentd monitor_agent host") 334 port := flag.String("port", "24220", "fluentd monitor_agent port") 335 pluginType := flag.String("plugin-type", "", "Gets the metric that matches this plugin type") 336 pluginIDPatternString := flag.String("plugin-id-pattern", "", "Gets the metric that matches this plugin id pattern") 337 prefix := flag.String("metric-key-prefix", "fluentd", "Metric key prefix") 338 tempFile := flag.String("tempfile", "", "Temp file name") 339 extendedMetricNames := flag.String("extended_metrics", "", "extended metric names joind with ',' or 'all' (fluentd >= v1.6.0)") 340 workers := flag.Uint("workers", 1, "specifying the number of Fluentd's multi-process workers") 341 flag.Parse() 342 343 var pluginIDPattern *regexp.Regexp 344 var err error 345 if *pluginIDPatternString != "" { 346 pluginIDPattern, err = regexp.Compile(*pluginIDPatternString) 347 if err != nil { 348 fmt.Fprintf(os.Stderr, "failed to exec mackerel-plugin-fluentd: invalid plugin-id-pattern: %s\n", err) 349 os.Exit(1) 350 } 351 } 352 353 var extendedMetrics []string 354 switch *extendedMetricNames { 355 case "all": 356 for key := range extendedGraphs { 357 extendedMetrics = append(extendedMetrics, key) 358 } 359 case "": 360 default: 361 for _, name := range strings.Split(*extendedMetricNames, ",") { 362 fullName := metricName(name) 363 if _, exists := extendedGraphs[fullName]; !exists { 364 fmt.Fprintf(os.Stderr, "extended_metrics %s is not supported. See also https://www.fluentd.org/blog/fluentd-v1.6.0-has-been-released\n", name) 365 os.Exit(1) 366 } 367 extendedMetrics = append(extendedMetrics, name) 368 } 369 } 370 f := FluentdPlugin{ 371 Host: *host, 372 Port: *port, 373 Prefix: *prefix, 374 Tempfile: *tempFile, 375 pluginType: *pluginType, 376 pluginIDPattern: pluginIDPattern, 377 extendedMetrics: extendedMetrics, 378 Workers: *workers, 379 } 380 381 helper := mp.NewMackerelPlugin(f) 382 383 helper.Tempfile = *tempFile 384 if *tempFile == "" { 385 tempFileSuffix := []string{*host, *port} 386 if *pluginType != "" { 387 tempFileSuffix = append(tempFileSuffix, *pluginType) 388 } 389 if *pluginIDPatternString != "" { 390 tempFileSuffix = append(tempFileSuffix, fmt.Sprintf("%x", md5.Sum([]byte(*pluginIDPatternString)))) 391 } 392 helper.SetTempfileByBasename(fmt.Sprintf("mackerel-plugin-fluentd-%s", strings.Join(tempFileSuffix, "-"))) 393 } 394 395 helper.Run() 396 }