smira / go-statsd

Go statsd client library with zero allocation overhead, great performance and reconnects
MIT License
109 stars 18 forks source link

export `Tag` fields #26

Closed gfreezy closed 4 years ago

gfreezy commented 4 years ago

Export Tag fields.

: , = " are not valid characters in statsd line protocol. I need to remove these characters before sending to go-statsd client.

closed #25

codecov[bot] commented 4 years ago

Codecov Report

Merging #26 into master will not change coverage. The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##           master      #26   +/-   ##
=======================================
  Coverage   88.05%   88.05%           
=======================================
  Files           5        5           
  Lines         335      335           
=======================================
  Hits          295      295           
  Misses         37       37           
  Partials        3        3           
Impacted Files Coverage Δ
tags.go 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update 448e76c...507b11d. Read the comment docs.

smira commented 4 years ago

I feel these fields are internal details of the implementation.

The only way to create a tag is via public functions, can you validate before creating a tag?

gfreezy commented 4 years ago

Currently i am doing like this, copy Tag related code to my own repo. This looks a bit verbose. Wish there is a better way than duplicating the code from go-statsd.

package metrics

import (
    "github.com/devopsfaith/krakend/config"
    "github.com/devopsfaith/krakend/logging"
    "github.com/mitchellh/mapstructure"
    "github.com/smira/go-statsd"
    "os"
    "strings"
    "time"
)

const Namespace = "github_com/xiachufang/krakend-ce/pkg/metrics"
const (
    typeString = iota
    typeInt64
)

// Tag is metric-specific tag
type Tag struct {
    name     string
    strvalue string
    intvalue int64
    typ      byte
}

type Metrics struct {
    statsdClient *statsd.Client
}

type metricsConfig struct {
    Address string
    Prefix  string
}

func NewMetrics(config config.ExtraConfig, l logging.Logger) *Metrics {
    raw := config[Namespace]
    var metricsConfig metricsConfig
    err := mapstructure.Decode(raw, &metricsConfig)
    if err != nil {
        return &Metrics{
            statsdClient: nil,
        }
    }
    hostname, err := os.Hostname()
    if err != nil {
        l.Warning("get hostname error")
        hostname = "unknown"
    }

    return &Metrics{
        statsdClient: statsd.NewClient(metricsConfig.Address, func(options *statsd.ClientOptions) {
            options.DefaultTags = []statsd.Tag{statsd.StringTag("server", hostname)}
            if !strings.HasSuffix(metricsConfig.Prefix, ".") {
                metricsConfig.Prefix += "."
            }
            options.MetricPrefix = metricsConfig.Prefix
            options.FlushInterval = time.Second
            options.ReconnectInterval = 5 * time.Minute
        }),
    }
}

func (metrics *Metrics) Incr(name string, value int64, tags ...Tag) {
    if metrics.statsdClient != nil {
        metrics.statsdClient.Incr(name, value, sanitizeTags(tags)...)
    }
}

func (metrics *Metrics) Decr(name string, value int64, tags ...Tag) {
    if metrics.statsdClient != nil {
        metrics.statsdClient.Incr(name, value, sanitizeTags(tags)...)
    }
}

func (metrics *Metrics) Gauge(name string, value int64, tags ...Tag) {
    if metrics.statsdClient != nil {
        metrics.statsdClient.Gauge(name, value, sanitizeTags(tags)...)
    }
}

func (metrics *Metrics) Timing(name string, value int64, tags ...Tag) {
    if metrics.statsdClient != nil {
        metrics.statsdClient.Timing(name, value, sanitizeTags(tags)...)
    }
}

func (metrics *Metrics) PrecisionTiming(name string, value time.Duration, tags ...Tag) {
    if metrics.statsdClient != nil {
        metrics.statsdClient.PrecisionTiming(name, value, sanitizeTags(tags)...)
    }
}

// StringTag creates Tag with string value
func StringTag(name, value string) Tag {
    return Tag{name: name, strvalue: value, typ: typeString}
}

// IntTag creates Tag with integer value
func IntTag(name string, value int) Tag {
    return Tag{name: name, intvalue: int64(value), typ: typeInt64}
}

// Int64Tag creates Tag with integer value
func Int64Tag(name string, value int64) Tag {
    return Tag{name: name, intvalue: value, typ: typeInt64}
}

func sanitizeTags(tags []Tag) []statsd.Tag {
    var sanitizedTags []statsd.Tag
    for _, tag := range tags {
        if tag.typ == typeInt64 {
            sanitizedTags = append(sanitizedTags, statsd.Int64Tag(sanitizeValue(tag.name), tag.intvalue))
        } else if tag.typ == typeString {
            sanitizedTags = append(sanitizedTags, statsd.StringTag(sanitizeValue(tag.name), sanitizeValue(tag.strvalue)))
        }
    }
    return sanitizedTags
}

func sanitizeValue(v string) string {
    v = strings.ReplaceAll(v, ":", "_")
    v = strings.ReplaceAll(v, ",", "_")
    v = strings.ReplaceAll(v, " ", "_")
    v = strings.ReplaceAll(v, "\"", "'")
    v = strings.ReplaceAll(v, "=", "_")
    return v
}
smira commented 4 years ago

can you then just provide your own implementations of StringTag (and IntTag, ..), which does sanitizeValue, return your own type Tag which you will typecast back to statsd.Tag before sending it to Incr and other funcs?

what you're doing is just wrapping statsd library with another layer of extra sanitization/features

gfreezy commented 4 years ago

got it.