spectator/meter/id.go (134 lines of code) (raw):

package meter import ( "fmt" "sort" "strings" "sync" ) // Id represents a meter's identifying information and dimensions (tags). type Id struct { name string tags map[string]string // keyOnce protects access to key, allowing it to be computed on demand // without racing other readers. keyOnce sync.Once key string // spectatordId is the Id formatted for spectatord line protocol spectatordId string } var builderPool = &sync.Pool{ New: func() interface{} { return &strings.Builder{} }, } // MapKey computes and saves a key within the struct to be used to uniquely // identify this *Id in a map. This does use the information from within the // *Id, so it assumes you've not accidentally double-declared this *Id. func (id *Id) MapKey() string { id.keyOnce.Do(func() { // if the key was set directly during Id construction, then do not // compute a value. if id.key != "" { return } buf := builderPool.Get().(*strings.Builder) buf.Reset() defer builderPool.Put(buf) const errKey = "ERR" id.key = func() string { _, err := buf.WriteString(id.name) if err != nil { return errKey } keys := make([]string, 0, len(id.tags)) for k := range id.tags { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { v := id.tags[k] _, err = buf.WriteRune('|') if err != nil { return errKey } _, err = buf.WriteString(k) if err != nil { return errKey } _, err = buf.WriteRune('|') if err != nil { return errKey } _, err = buf.WriteString(v) if err != nil { return errKey } } return buf.String() }() }) return id.key } // NewId generates a new *Id from the metric name, and the tags you want to // include on your metric. func NewId(name string, tags map[string]string) *Id { myTags := make(map[string]string) for k, v := range tags { myTags[k] = v } spectatorId := toSpectatorId(name, tags) return &Id{ name: name, tags: myTags, spectatordId: spectatorId, } } // WithTag creates a deep copy of the *Id, adding the requested tag to the // internal collection. func (id *Id) WithTag(key string, value string) *Id { newTags := make(map[string]string) for k, v := range id.tags { newTags[k] = v } newTags[key] = value return NewId(id.name, newTags) } func (id *Id) String() string { return fmt.Sprintf("Id{name=%s,tags=%v}", id.name, id.tags) } // Name exposes the internal metric name field. func (id *Id) Name() string { return id.name } // Tags directly exposes the internal tags map. This is not a copy of the map, // so any modifications to it will be observed by the *Id. func (id *Id) Tags() map[string]string { return id.tags } // WithTags takes a map of tags, and returns a deep copy of *Id with the new // tags appended to the original ones. Overlapping keys are overwritten. If the // input to this method is empty, this does not return a deep copy of *Id. func (id *Id) WithTags(tags map[string]string) *Id { if len(tags) == 0 { return id } newTags := make(map[string]string) for k, v := range id.tags { newTags[k] = v } for k, v := range tags { newTags[k] = v } return NewId(id.name, newTags) } func toSpectatorId(name string, tags map[string]string) string { result := replaceInvalidCharacters(name) for k, v := range tags { k = replaceInvalidCharacters(k) v = replaceInvalidCharacters(v) result += fmt.Sprintf(",%s=%s", k, v) } return result } func replaceInvalidCharacters(input string) string { var result strings.Builder for _, r := range input { if !isValidCharacter(r) { result.WriteRune('_') } else { result.WriteRune(r) } } return result.String() } func isValidCharacter(r rune) bool { return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '.' || r == '_' || r == '~' || r == '^' }