166 lines
3.9 KiB
Go
166 lines
3.9 KiB
Go
package graphite
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/influxdata/telegraf"
|
|
)
|
|
|
|
const DEFAULT_TEMPLATE = "host.tags.measurement.field"
|
|
|
|
var (
|
|
allowedChars = regexp.MustCompile(`[^a-zA-Z0-9-:._=\p{L}]`)
|
|
hypenChars = strings.NewReplacer(
|
|
"/", "-",
|
|
"@", "-",
|
|
"*", "-",
|
|
)
|
|
dropChars = strings.NewReplacer(
|
|
`\`, "",
|
|
"..", ".",
|
|
)
|
|
|
|
fieldDeleter = strings.NewReplacer(".FIELDNAME", "", "FIELDNAME.", "")
|
|
)
|
|
|
|
type GraphiteSerializer struct {
|
|
Prefix string
|
|
Template string
|
|
}
|
|
|
|
func (s *GraphiteSerializer) Serialize(metric telegraf.Metric) ([]byte, error) {
|
|
out := []byte{}
|
|
|
|
// 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() {
|
|
switch v := value.(type) {
|
|
case string:
|
|
continue
|
|
case bool:
|
|
if v {
|
|
value = 1
|
|
} else {
|
|
value = 0
|
|
}
|
|
}
|
|
metricString := fmt.Sprintf("%s %#v %d\n",
|
|
// insert "field" section of template
|
|
sanitize(InsertField(bucket, fieldName)),
|
|
value,
|
|
timestamp)
|
|
point := []byte(metricString)
|
|
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 GraphiteSerializer.Template
|
|
// to generate this, or DEFAULT_TEMPLATE.
|
|
//
|
|
// 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 GraphiteSerializer.InsertField() function.
|
|
func SerializeBucketName(
|
|
measurement string,
|
|
tags map[string]string,
|
|
template string,
|
|
prefix string,
|
|
) string {
|
|
if template == "" {
|
|
template = DEFAULT_TEMPLATE
|
|
}
|
|
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 {
|
|
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 tag_str string
|
|
for i, k := range keys {
|
|
tag_value := strings.Replace(tags[k], ".", "_", -1)
|
|
if i == 0 {
|
|
tag_str += tag_value
|
|
} else {
|
|
tag_str += "." + tag_value
|
|
}
|
|
}
|
|
return tag_str
|
|
}
|
|
|
|
func sanitize(value string) string {
|
|
// Apply special hypenation rules to preserve backwards compatibility
|
|
value = hypenChars.Replace(value)
|
|
// Apply rule to drop some chars to preserve backwards compatibility
|
|
value = dropChars.Replace(value)
|
|
// Replace any remaining illegal chars
|
|
return allowedChars.ReplaceAllLiteralString(value, "_")
|
|
}
|