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  }