550 lines
16 KiB
Go
550 lines
16 KiB
Go
package elasticsearch
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"crypto/sha256"
|
|
|
|
"github.com/olivere/elastic"
|
|
|
|
"github.com/influxdata/telegraf"
|
|
"github.com/influxdata/telegraf/config"
|
|
"github.com/influxdata/telegraf/plugins/common/tls"
|
|
"github.com/influxdata/telegraf/plugins/outputs"
|
|
)
|
|
|
|
type Elasticsearch struct {
|
|
AuthBearerToken string `toml:"auth_bearer_token"`
|
|
DefaultPipeline string `toml:"default_pipeline"`
|
|
DefaultTagValue string `toml:"default_tag_value"`
|
|
EnableGzip bool `toml:"enable_gzip"`
|
|
EnableSniffer bool `toml:"enable_sniffer"`
|
|
FloatHandling string `toml:"float_handling"`
|
|
FloatReplacement float64 `toml:"float_replacement_value"`
|
|
ForceDocumentID bool `toml:"force_document_id"`
|
|
HealthCheckInterval config.Duration `toml:"health_check_interval"`
|
|
IndexName string `toml:"index_name"`
|
|
ManageTemplate bool `toml:"manage_template"`
|
|
OverwriteTemplate bool `toml:"overwrite_template"`
|
|
Password string `toml:"password"`
|
|
TemplateName string `toml:"template_name"`
|
|
Timeout config.Duration `toml:"timeout"`
|
|
URLs []string `toml:"urls"`
|
|
UsePipeline string `toml:"use_pipeline"`
|
|
Username string `toml:"username"`
|
|
Log telegraf.Logger `toml:"-"`
|
|
majorReleaseNumber int
|
|
pipelineName string
|
|
pipelineTagKeys []string
|
|
tagKeys []string
|
|
tls.ClientConfig
|
|
|
|
Client *elastic.Client
|
|
}
|
|
|
|
var sampleConfig = `
|
|
## The full HTTP endpoint URL for your Elasticsearch instance
|
|
## Multiple urls can be specified as part of the same cluster,
|
|
## this means that only ONE of the urls will be written to each interval.
|
|
urls = [ "http://node1.es.example.com:9200" ] # required.
|
|
## Elasticsearch client timeout, defaults to "5s" if not set.
|
|
timeout = "5s"
|
|
## Set to true to ask Elasticsearch a list of all cluster nodes,
|
|
## thus it is not necessary to list all nodes in the urls config option.
|
|
enable_sniffer = false
|
|
## Set to true to enable gzip compression
|
|
enable_gzip = false
|
|
## Set the interval to check if the Elasticsearch nodes are available
|
|
## Setting to "0s" will disable the health check (not recommended in production)
|
|
health_check_interval = "10s"
|
|
## HTTP basic authentication details
|
|
# username = "telegraf"
|
|
# password = "mypassword"
|
|
## HTTP bearer token authentication details
|
|
# auth_bearer_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
|
|
|
## Index Config
|
|
## The target index for metrics (Elasticsearch will create if it not exists).
|
|
## You can use the date specifiers below to create indexes per time frame.
|
|
## The metric timestamp will be used to decide the destination index name
|
|
# %Y - year (2016)
|
|
# %y - last two digits of year (00..99)
|
|
# %m - month (01..12)
|
|
# %d - day of month (e.g., 01)
|
|
# %H - hour (00..23)
|
|
# %V - week of the year (ISO week) (01..53)
|
|
## Additionally, you can specify a tag name using the notation {{tag_name}}
|
|
## which will be used as part of the index name. If the tag does not exist,
|
|
## the default tag value will be used.
|
|
# index_name = "telegraf-{{host}}-%Y.%m.%d"
|
|
# default_tag_value = "none"
|
|
index_name = "telegraf-%Y.%m.%d" # required.
|
|
|
|
## Optional TLS Config
|
|
# tls_ca = "/etc/telegraf/ca.pem"
|
|
# tls_cert = "/etc/telegraf/cert.pem"
|
|
# tls_key = "/etc/telegraf/key.pem"
|
|
## Use TLS but skip chain & host verification
|
|
# insecure_skip_verify = false
|
|
|
|
## Template Config
|
|
## Set to true if you want telegraf to manage its index template.
|
|
## If enabled it will create a recommended index template for telegraf indexes
|
|
manage_template = true
|
|
## The template name used for telegraf indexes
|
|
template_name = "telegraf"
|
|
## Set to true if you want telegraf to overwrite an existing template
|
|
overwrite_template = false
|
|
## If set to true a unique ID hash will be sent as sha256(concat(timestamp,measurement,series-hash)) string
|
|
## it will enable data resend and update metric points avoiding duplicated metrics with diferent id's
|
|
force_document_id = false
|
|
|
|
## Specifies the handling of NaN and Inf values.
|
|
## This option can have the following values:
|
|
## none -- do not modify field-values (default); will produce an error if NaNs or infs are encountered
|
|
## drop -- drop fields containing NaNs or infs
|
|
## replace -- replace with the value in "float_replacement_value" (default: 0.0)
|
|
## NaNs and inf will be replaced with the given number, -inf with the negative of that number
|
|
# float_handling = "none"
|
|
# float_replacement_value = 0.0
|
|
|
|
## Pipeline Config
|
|
## To use a ingest pipeline, set this to the name of the pipeline you want to use.
|
|
# use_pipeline = "my_pipeline"
|
|
## Additionally, you can specify a tag name using the notation {{tag_name}}
|
|
## which will be used as part of the pipeline name. If the tag does not exist,
|
|
## the default pipeline will be used as the pipeline. If no default pipeline is set,
|
|
## no pipeline is used for the metric.
|
|
# use_pipeline = "{{es_pipeline}}"
|
|
# default_pipeline = "my_pipeline"
|
|
`
|
|
|
|
const telegrafTemplate = `
|
|
{
|
|
{{ if (lt .Version 6) }}
|
|
"template": "{{.TemplatePattern}}",
|
|
{{ else }}
|
|
"index_patterns" : [ "{{.TemplatePattern}}" ],
|
|
{{ end }}
|
|
"settings": {
|
|
"index": {
|
|
"refresh_interval": "10s",
|
|
"mapping.total_fields.limit": 5000,
|
|
"auto_expand_replicas" : "0-1",
|
|
"codec" : "best_compression"
|
|
}
|
|
},
|
|
"mappings" : {
|
|
{{ if (lt .Version 7) }}
|
|
"metrics" : {
|
|
{{ if (lt .Version 6) }}
|
|
"_all": { "enabled": false },
|
|
{{ end }}
|
|
{{ end }}
|
|
"properties" : {
|
|
"@timestamp" : { "type" : "date" },
|
|
"measurement_name" : { "type" : "keyword" }
|
|
},
|
|
"dynamic_templates": [
|
|
{
|
|
"tags": {
|
|
"match_mapping_type": "string",
|
|
"path_match": "tag.*",
|
|
"mapping": {
|
|
"ignore_above": 512,
|
|
"type": "keyword"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"metrics_long": {
|
|
"match_mapping_type": "long",
|
|
"mapping": {
|
|
"type": "float",
|
|
"index": false
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"metrics_double": {
|
|
"match_mapping_type": "double",
|
|
"mapping": {
|
|
"type": "float",
|
|
"index": false
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"text_fields": {
|
|
"match": "*",
|
|
"mapping": {
|
|
"norms": false
|
|
}
|
|
}
|
|
}
|
|
]
|
|
{{ if (lt .Version 7) }}
|
|
}
|
|
{{ end }}
|
|
}
|
|
}`
|
|
|
|
type templatePart struct {
|
|
TemplatePattern string
|
|
Version int
|
|
}
|
|
|
|
func (a *Elasticsearch) Connect() error {
|
|
if a.URLs == nil || a.IndexName == "" {
|
|
return fmt.Errorf("elasticsearch urls or index_name is not defined")
|
|
}
|
|
|
|
// Determine if we should process NaN and inf values
|
|
switch a.FloatHandling {
|
|
case "", "none":
|
|
a.FloatHandling = "none"
|
|
case "drop", "replace":
|
|
default:
|
|
return fmt.Errorf("invalid float_handling type %q", a.FloatHandling)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(a.Timeout))
|
|
defer cancel()
|
|
|
|
var clientOptions []elastic.ClientOptionFunc
|
|
|
|
tlsCfg, err := a.ClientConfig.TLSConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tr := &http.Transport{
|
|
TLSClientConfig: tlsCfg,
|
|
}
|
|
|
|
httpclient := &http.Client{
|
|
Transport: tr,
|
|
Timeout: time.Duration(a.Timeout),
|
|
}
|
|
|
|
elasticURL, err := url.Parse(a.URLs[0])
|
|
if err != nil {
|
|
return fmt.Errorf("parsing URL failed: %v", err)
|
|
}
|
|
|
|
clientOptions = append(clientOptions,
|
|
elastic.SetHttpClient(httpclient),
|
|
elastic.SetSniff(a.EnableSniffer),
|
|
elastic.SetScheme(elasticURL.Scheme),
|
|
elastic.SetURL(a.URLs...),
|
|
elastic.SetHealthcheckInterval(time.Duration(a.HealthCheckInterval)),
|
|
elastic.SetGzip(a.EnableGzip),
|
|
)
|
|
|
|
if a.Username != "" && a.Password != "" {
|
|
clientOptions = append(clientOptions,
|
|
elastic.SetBasicAuth(a.Username, a.Password),
|
|
)
|
|
}
|
|
|
|
if a.AuthBearerToken != "" {
|
|
clientOptions = append(clientOptions,
|
|
elastic.SetHeaders(http.Header{
|
|
"Authorization": []string{fmt.Sprintf("Bearer %s", a.AuthBearerToken)},
|
|
}),
|
|
)
|
|
}
|
|
|
|
if time.Duration(a.HealthCheckInterval) == 0 {
|
|
clientOptions = append(clientOptions,
|
|
elastic.SetHealthcheck(false),
|
|
)
|
|
a.Log.Debugf("Disabling health check")
|
|
}
|
|
|
|
client, err := elastic.NewClient(clientOptions...)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check for ES version on first node
|
|
esVersion, err := client.ElasticsearchVersion(a.URLs[0])
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("elasticsearch version check failed: %s", err)
|
|
}
|
|
|
|
// quit if ES version is not supported
|
|
majorReleaseNumber, err := strconv.Atoi(strings.Split(esVersion, ".")[0])
|
|
if err != nil || majorReleaseNumber < 5 {
|
|
return fmt.Errorf("elasticsearch version not supported: %s", esVersion)
|
|
}
|
|
|
|
a.Log.Infof("Elasticsearch version: %q", esVersion)
|
|
|
|
a.Client = client
|
|
a.majorReleaseNumber = majorReleaseNumber
|
|
|
|
if a.ManageTemplate {
|
|
err := a.manageTemplate(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
a.IndexName, a.tagKeys = a.GetTagKeys(a.IndexName)
|
|
a.pipelineName, a.pipelineTagKeys = a.GetTagKeys(a.UsePipeline)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetPointID generates a unique ID for a Metric Point
|
|
func GetPointID(m telegraf.Metric) string {
|
|
var buffer bytes.Buffer
|
|
//Timestamp(ns),measurement name and Series Hash for compute the final SHA256 based hash ID
|
|
|
|
buffer.WriteString(strconv.FormatInt(m.Time().Local().UnixNano(), 10)) //nolint:revive // from buffer.go: "err is always nil"
|
|
buffer.WriteString(m.Name()) //nolint:revive // from buffer.go: "err is always nil"
|
|
buffer.WriteString(strconv.FormatUint(m.HashID(), 10)) //nolint:revive // from buffer.go: "err is always nil"
|
|
|
|
return fmt.Sprintf("%x", sha256.Sum256(buffer.Bytes()))
|
|
}
|
|
|
|
func (a *Elasticsearch) Write(metrics []telegraf.Metric) error {
|
|
if len(metrics) == 0 {
|
|
return nil
|
|
}
|
|
|
|
bulkRequest := a.Client.Bulk()
|
|
|
|
for _, metric := range metrics {
|
|
var name = metric.Name()
|
|
|
|
// index name has to be re-evaluated each time for telegraf
|
|
// to send the metric to the correct time-based index
|
|
indexName := a.GetIndexName(a.IndexName, metric.Time(), a.tagKeys, metric.Tags())
|
|
|
|
// Handle NaN and inf field-values
|
|
fields := make(map[string]interface{})
|
|
for k, value := range metric.Fields() {
|
|
v, ok := value.(float64)
|
|
if !ok || a.FloatHandling == "none" || !(math.IsNaN(v) || math.IsInf(v, 0)) {
|
|
fields[k] = value
|
|
continue
|
|
}
|
|
if a.FloatHandling == "drop" {
|
|
continue
|
|
}
|
|
|
|
if math.IsNaN(v) || math.IsInf(v, 1) {
|
|
fields[k] = a.FloatReplacement
|
|
} else {
|
|
fields[k] = -a.FloatReplacement
|
|
}
|
|
}
|
|
|
|
m := make(map[string]interface{})
|
|
|
|
m["@timestamp"] = metric.Time()
|
|
m["measurement_name"] = name
|
|
m["tag"] = metric.Tags()
|
|
m[name] = fields
|
|
|
|
br := elastic.NewBulkIndexRequest().Index(indexName).Doc(m)
|
|
|
|
if a.ForceDocumentID {
|
|
id := GetPointID(metric)
|
|
br.Id(id)
|
|
}
|
|
|
|
if a.majorReleaseNumber <= 6 {
|
|
br.Type("metrics")
|
|
}
|
|
|
|
if a.UsePipeline != "" {
|
|
if pipelineName := a.getPipelineName(a.pipelineName, a.pipelineTagKeys, metric.Tags()); pipelineName != "" {
|
|
br.Pipeline(pipelineName)
|
|
}
|
|
}
|
|
|
|
bulkRequest.Add(br)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(a.Timeout))
|
|
defer cancel()
|
|
|
|
res, err := bulkRequest.Do(ctx)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("error sending bulk request to Elasticsearch: %s", err)
|
|
}
|
|
|
|
if res.Errors {
|
|
for id, err := range res.Failed() {
|
|
a.Log.Errorf("Elasticsearch indexing failure, id: %d, error: %s, caused by: %s, %s", id, err.Error.Reason, err.Error.CausedBy["reason"], err.Error.CausedBy["type"])
|
|
break
|
|
}
|
|
return fmt.Errorf("elasticsearch failed to index %d metrics", len(res.Failed()))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *Elasticsearch) manageTemplate(ctx context.Context) error {
|
|
if a.TemplateName == "" {
|
|
return fmt.Errorf("elasticsearch template_name configuration not defined")
|
|
}
|
|
|
|
templateExists, errExists := a.Client.IndexTemplateExists(a.TemplateName).Do(ctx)
|
|
|
|
if errExists != nil {
|
|
return fmt.Errorf("elasticsearch template check failed, template name: %s, error: %s", a.TemplateName, errExists)
|
|
}
|
|
|
|
templatePattern := a.IndexName
|
|
|
|
if strings.Contains(templatePattern, "%") {
|
|
templatePattern = templatePattern[0:strings.Index(templatePattern, "%")]
|
|
}
|
|
|
|
if strings.Contains(templatePattern, "{{") {
|
|
templatePattern = templatePattern[0:strings.Index(templatePattern, "{{")]
|
|
}
|
|
|
|
if templatePattern == "" {
|
|
return fmt.Errorf("template cannot be created for dynamic index names without an index prefix")
|
|
}
|
|
|
|
if (a.OverwriteTemplate) || (!templateExists) || (templatePattern != "") {
|
|
tp := templatePart{
|
|
TemplatePattern: templatePattern + "*",
|
|
Version: a.majorReleaseNumber,
|
|
}
|
|
|
|
t := template.Must(template.New("template").Parse(telegrafTemplate))
|
|
var tmpl bytes.Buffer
|
|
|
|
if err := t.Execute(&tmpl, tp); err != nil {
|
|
return err
|
|
}
|
|
_, errCreateTemplate := a.Client.IndexPutTemplate(a.TemplateName).BodyString(tmpl.String()).Do(ctx)
|
|
|
|
if errCreateTemplate != nil {
|
|
return fmt.Errorf("elasticsearch failed to create index template %s : %s", a.TemplateName, errCreateTemplate)
|
|
}
|
|
|
|
a.Log.Debugf("Template %s created or updated\n", a.TemplateName)
|
|
} else {
|
|
a.Log.Debug("Found existing Elasticsearch template. Skipping template management")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Elasticsearch) GetTagKeys(indexName string) (string, []string) {
|
|
tagKeys := []string{}
|
|
startTag := strings.Index(indexName, "{{")
|
|
|
|
for startTag >= 0 {
|
|
endTag := strings.Index(indexName, "}}")
|
|
|
|
if endTag < 0 {
|
|
startTag = -1
|
|
} else {
|
|
tagName := indexName[startTag+2 : endTag]
|
|
|
|
var tagReplacer = strings.NewReplacer(
|
|
"{{"+tagName+"}}", "%s",
|
|
)
|
|
|
|
indexName = tagReplacer.Replace(indexName)
|
|
tagKeys = append(tagKeys, strings.TrimSpace(tagName))
|
|
|
|
startTag = strings.Index(indexName, "{{")
|
|
}
|
|
}
|
|
|
|
return indexName, tagKeys
|
|
}
|
|
|
|
func (a *Elasticsearch) GetIndexName(indexName string, eventTime time.Time, tagKeys []string, metricTags map[string]string) string {
|
|
if strings.Contains(indexName, "%") {
|
|
var dateReplacer = strings.NewReplacer(
|
|
"%Y", eventTime.UTC().Format("2006"),
|
|
"%y", eventTime.UTC().Format("06"),
|
|
"%m", eventTime.UTC().Format("01"),
|
|
"%d", eventTime.UTC().Format("02"),
|
|
"%H", eventTime.UTC().Format("15"),
|
|
"%V", getISOWeek(eventTime.UTC()),
|
|
)
|
|
|
|
indexName = dateReplacer.Replace(indexName)
|
|
}
|
|
|
|
tagValues := []interface{}{}
|
|
|
|
for _, key := range tagKeys {
|
|
if value, ok := metricTags[key]; ok {
|
|
tagValues = append(tagValues, value)
|
|
} else {
|
|
a.Log.Debugf("Tag '%s' not found, using '%s' on index name instead\n", key, a.DefaultTagValue)
|
|
tagValues = append(tagValues, a.DefaultTagValue)
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf(indexName, tagValues...)
|
|
}
|
|
|
|
func (a *Elasticsearch) getPipelineName(pipelineInput string, tagKeys []string, metricTags map[string]string) string {
|
|
if !strings.Contains(pipelineInput, "%") || len(tagKeys) == 0 {
|
|
return pipelineInput
|
|
}
|
|
|
|
var tagValues []interface{}
|
|
|
|
for _, key := range tagKeys {
|
|
if value, ok := metricTags[key]; ok {
|
|
tagValues = append(tagValues, value)
|
|
continue
|
|
}
|
|
a.Log.Debugf("Tag %s not found, reverting to default pipeline instead.", key)
|
|
return a.DefaultPipeline
|
|
}
|
|
return fmt.Sprintf(pipelineInput, tagValues...)
|
|
}
|
|
|
|
func getISOWeek(eventTime time.Time) string {
|
|
_, week := eventTime.ISOWeek()
|
|
return strconv.Itoa(week)
|
|
}
|
|
|
|
func (a *Elasticsearch) SampleConfig() string {
|
|
return sampleConfig
|
|
}
|
|
|
|
func (a *Elasticsearch) Description() string {
|
|
return "Configuration for Elasticsearch to send metrics to."
|
|
}
|
|
|
|
func (a *Elasticsearch) Close() error {
|
|
a.Client = nil
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
outputs.Add("elasticsearch", func() telegraf.Output {
|
|
return &Elasticsearch{
|
|
Timeout: config.Duration(time.Second * 5),
|
|
HealthCheckInterval: config.Duration(time.Second * 10),
|
|
}
|
|
})
|
|
}
|