telegraf/plugins/serializers/graphite/graphite.go

157 lines
3.9 KiB
Go

package graphite
import (
"fmt"
"sort"
"strings"
"github.com/influxdata/telegraf"
)
// DefaultTemplate const
const DefaultTemplate = "host.tags.measurement.field"
var (
fieldDeleter = strings.NewReplacer(".FIELDNAME", "", "FIELDNAME.", "")
sanitizedChars = strings.NewReplacer("/", "-", "@", "-", "*", "-", " ", "_", "..", ".", `\`, "", ")", "_", "(", "_")
)
// SerializerGraphite struct
type SerializerGraphite struct {
Prefix string
Template string
}
// Serialize ([]string, error)
func (s *SerializerGraphite) Serialize(metric telegraf.Metric) ([]string, error) {
out := []string{}
// Convert UnixNano to Unix timestamps
timestamp := metric.UnixNano() / 1000000000
bucket := SerializeBucketName(metric.Name(), metric.Tags(), s.Template, s.Prefix)
if bucket == "" {
return out, nil
}
for fieldName, value := range metric.Fields() {
// Convert value to string
valueS := fmt.Sprintf("%#v", value)
point := fmt.Sprintf("%s %s %d",
// insert "field" section of template
sanitizedChars.Replace(InsertField(bucket, fieldName)),
sanitizedChars.Replace(valueS),
timestamp)
out = append(out, point)
}
return out, nil
}
// SerializeBucketName will take the given measurement name and tags and
// produce a graphite bucket. It will use the Serializer.Template
// to generate this, or DefaultTemplate.
//
// NOTE: SerializeBucketName replaces the "field" portion of the template with
// FIELDNAME. It is up to the user to replace this. This is so that
// SerializeBucketName can be called just once per measurement, rather than
// once per field. See Serializer.InsertField() function.
func SerializeBucketName(
measurement string,
tags map[string]string,
template string,
prefix string,
) string {
if template == "" {
template = DefaultTemplate
}
tagsCopy := make(map[string]string)
for k, v := range tags {
tagsCopy[k] = v
}
var out []string
templateParts := strings.Split(template, ".")
for _, templatePart := range templateParts {
switch templatePart {
case "measurement":
out = append(out, measurement)
case "tags":
// we will replace this later
out = append(out, "TAGS")
case "field":
// user of SerializeBucketName needs to replace this
out = append(out, "FIELDNAME")
default:
// This is a tag being applied
if tagvalue, ok := tagsCopy[templatePart]; ok {
if templatePart == "host" {
hostSplit := strings.Split(tagvalue, ".")
for i := len(hostSplit) - 1; i >= 0; i-- {
out = append(out, hostSplit[i])
}
} else {
out = append(out, strings.Replace(tagvalue, ".", "_", -1))
}
delete(tagsCopy, templatePart)
}
}
}
// insert remaining tags into output name
for i, templatePart := range out {
if templatePart == "TAGS" {
out[i] = buildTags(tagsCopy)
break
}
}
if len(out) == 0 {
return ""
}
if prefix == "" {
return strings.Join(out, ".")
}
return prefix + "." + strings.Join(out, ".")
}
// InsertField takes the bucket string from SerializeBucketName and replaces the
// FIELDNAME portion. If fieldName == "value", it will simply delete the
// FIELDNAME portion.
func InsertField(bucket, fieldName string) string {
// if the field name is "value", then dont use it
if fieldName == "value" {
return fieldDeleter.Replace(bucket)
}
return strings.Replace(bucket, "FIELDNAME", fieldName, 1)
}
func buildTags(tags map[string]string) string {
var keys []string
for k := range tags {
keys = append(keys, k)
}
sort.Strings(keys)
var tagStr string
var tagValue string
var reversedHost []string
for i, k := range keys {
if k == "host" {
hostSplit := strings.Split(tags[k], ".")
for i := len(hostSplit) - 1; i >= 0; i-- {
reversedHost = append(reversedHost, hostSplit[i])
}
tagValue = strings.Join(reversedHost, ".")
} else {
tagValue = strings.Replace(tags[k], ".", "_", -1)
}
if i == 0 {
tagStr += tagValue
} else {
tagStr += "." + tagValue
}
}
return tagStr
}