vitess.io/vitess@v0.16.2/go/stats/export.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package stats is a wrapper for expvar. It additionally
    18  // exports new types that can be used to track performance.
    19  // It also provides a callback hook that allows a program
    20  // to export the variables using methods other than /debug/vars.
    21  // All variables support a String function that
    22  // is expected to return a JSON representation
    23  // of the variable.
    24  // Any function named Add will add the specified
    25  // number to the variable.
    26  // Any function named Counts returns a map of counts
    27  // that can be used by Rates to track rates over time.
    28  package stats
    29  
    30  import (
    31  	"bytes"
    32  	"expvar"
    33  	"fmt"
    34  	"strconv"
    35  	"strings"
    36  	"sync"
    37  	"time"
    38  
    39  	"github.com/spf13/pflag"
    40  
    41  	"vitess.io/vitess/go/vt/log"
    42  )
    43  
    44  var (
    45  	emitStats         bool
    46  	statsEmitPeriod   = 60 * time.Second
    47  	statsBackend      string
    48  	combineDimensions string
    49  	dropVariables     string
    50  )
    51  
    52  // CommonTags is a comma-separated list of common tags for stats backends
    53  var CommonTags []string
    54  
    55  func RegisterFlags(fs *pflag.FlagSet) {
    56  	fs.BoolVar(&emitStats, "emit_stats", emitStats, "If set, emit stats to push-based monitoring and stats backends")
    57  	fs.DurationVar(&statsEmitPeriod, "stats_emit_period", statsEmitPeriod, "Interval between emitting stats to all registered backends")
    58  	fs.StringVar(&statsBackend, "stats_backend", statsBackend, "The name of the registered push-based monitoring/stats backend to use")
    59  	fs.StringVar(&combineDimensions, "stats_combine_dimensions", combineDimensions, `List of dimensions to be combined into a single "all" value in exported stats vars`)
    60  	fs.StringVar(&dropVariables, "stats_drop_variables", dropVariables, `Variables to be dropped from the list of exported variables.`)
    61  	fs.StringSliceVar(&CommonTags, "stats_common_tags", CommonTags, `Comma-separated list of common tags for the stats backend. It provides both label and values. Example: label1:value1,label2:value2`)
    62  }
    63  
    64  // StatsAllStr is the consolidated name if a dimension gets combined.
    65  const StatsAllStr = "all"
    66  
    67  // NewVarHook is the type of a hook to export variables in a different way
    68  type NewVarHook func(name string, v expvar.Var)
    69  
    70  type varGroup struct {
    71  	sync.Mutex
    72  	vars       map[string]expvar.Var
    73  	newVarHook NewVarHook
    74  }
    75  
    76  func (vg *varGroup) register(nvh NewVarHook) {
    77  	vg.Lock()
    78  	defer vg.Unlock()
    79  	if vg.newVarHook != nil {
    80  		panic("You've already registered a function")
    81  	}
    82  	if nvh == nil {
    83  		panic("nil not allowed")
    84  	}
    85  	vg.newVarHook = nvh
    86  	// Call hook on existing vars because some might have been
    87  	// created before the call to register
    88  	for k, v := range vg.vars {
    89  		nvh(k, v)
    90  	}
    91  	vg.vars = nil
    92  }
    93  
    94  func (vg *varGroup) publish(name string, v expvar.Var) {
    95  	if isVarDropped(name) {
    96  		return
    97  	}
    98  	vg.Lock()
    99  	defer vg.Unlock()
   100  
   101  	expvar.Publish(name, v)
   102  	if vg.newVarHook != nil {
   103  		vg.newVarHook(name, v)
   104  	} else {
   105  		vg.vars[name] = v
   106  	}
   107  }
   108  
   109  var defaultVarGroup = varGroup{vars: make(map[string]expvar.Var)}
   110  
   111  // Register allows you to register a callback function
   112  // that will be called whenever a new stats variable gets
   113  // created. This can be used to build alternate methods
   114  // of exporting stats variables.
   115  func Register(nvh NewVarHook) {
   116  	defaultVarGroup.register(nvh)
   117  }
   118  
   119  // Publish is expvar.Publish+hook
   120  func Publish(name string, v expvar.Var) {
   121  	publish(name, v)
   122  }
   123  
   124  func publish(name string, v expvar.Var) {
   125  	defaultVarGroup.publish(name, v)
   126  }
   127  
   128  // PushBackend is an interface for any stats/metrics backend that requires data
   129  // to be pushed to it. It's used to support push-based metrics backends, as expvar
   130  // by default only supports pull-based ones.
   131  type PushBackend interface {
   132  	// PushAll pushes all stats from expvar to the backend
   133  	PushAll() error
   134  }
   135  
   136  var pushBackends = make(map[string]PushBackend)
   137  var pushBackendsLock sync.Mutex
   138  var once sync.Once
   139  
   140  // RegisterPushBackend allows modules to register PushBackend implementations.
   141  // Should be called on init().
   142  func RegisterPushBackend(name string, backend PushBackend) {
   143  	pushBackendsLock.Lock()
   144  	defer pushBackendsLock.Unlock()
   145  	if _, ok := pushBackends[name]; ok {
   146  		log.Fatalf("PushBackend %s already exists; can't register the same name multiple times", name)
   147  	}
   148  	pushBackends[name] = backend
   149  	if emitStats {
   150  		// Start a single goroutine to emit stats periodically
   151  		once.Do(func() {
   152  			go emitToBackend(&statsEmitPeriod)
   153  		})
   154  	}
   155  }
   156  
   157  // emitToBackend does a periodic emit to the selected PushBackend. If a push fails,
   158  // it will be logged as a warning (but things will otherwise proceed as normal).
   159  func emitToBackend(emitPeriod *time.Duration) {
   160  	ticker := time.NewTicker(*emitPeriod)
   161  	defer ticker.Stop()
   162  	for range ticker.C {
   163  		backend, ok := pushBackends[statsBackend]
   164  		if !ok {
   165  			log.Errorf("No PushBackend registered with name %s", statsBackend)
   166  			return
   167  		}
   168  		err := backend.PushAll()
   169  		if err != nil {
   170  			// TODO(aaijazi): This might cause log spam...
   171  			log.Warningf("Pushing stats to backend %v failed: %v", statsBackend, err)
   172  		}
   173  	}
   174  }
   175  
   176  // FloatFunc converts a function that returns
   177  // a float64 as an expvar.
   178  type FloatFunc func() float64
   179  
   180  // Help returns the help string (undefined currently)
   181  func (f FloatFunc) Help() string {
   182  	return "help"
   183  }
   184  
   185  // String is the implementation of expvar.var
   186  func (f FloatFunc) String() string {
   187  	return strconv.FormatFloat(f(), 'g', -1, 64)
   188  }
   189  
   190  // String is expvar.String+Get+hook
   191  type String struct {
   192  	mu sync.Mutex
   193  	s  string
   194  }
   195  
   196  // NewString returns a new String
   197  func NewString(name string) *String {
   198  	v := new(String)
   199  	publish(name, v)
   200  	return v
   201  }
   202  
   203  // Set sets the value
   204  func (v *String) Set(value string) {
   205  	v.mu.Lock()
   206  	v.s = value
   207  	v.mu.Unlock()
   208  }
   209  
   210  // Get returns the value
   211  func (v *String) Get() string {
   212  	v.mu.Lock()
   213  	s := v.s
   214  	v.mu.Unlock()
   215  	return s
   216  }
   217  
   218  // String is the implementation of expvar.var
   219  func (v *String) String() string {
   220  	return strconv.Quote(v.Get())
   221  }
   222  
   223  // StringFunc converts a function that returns
   224  // an string as an expvar.
   225  type StringFunc func() string
   226  
   227  // String is the implementation of expvar.var
   228  func (f StringFunc) String() string {
   229  	return strconv.Quote(f())
   230  }
   231  
   232  // JSONFunc is the public type for a single function that returns json directly.
   233  type JSONFunc func() string
   234  
   235  // String is the implementation of expvar.var
   236  func (f JSONFunc) String() string {
   237  	return f()
   238  }
   239  
   240  // PublishJSONFunc publishes any function that returns
   241  // a JSON string as a variable. The string is sent to
   242  // expvar as is.
   243  func PublishJSONFunc(name string, f func() string) {
   244  	publish(name, JSONFunc(f))
   245  }
   246  
   247  // StringMapFunc is the function equivalent of StringMap
   248  type StringMapFunc func() map[string]string
   249  
   250  // String is used by expvar.
   251  func (f StringMapFunc) String() string {
   252  	m := f()
   253  	if m == nil {
   254  		return "{}"
   255  	}
   256  	return stringMapToString(m)
   257  }
   258  
   259  func stringMapToString(m map[string]string) string {
   260  	b := bytes.NewBuffer(make([]byte, 0, 4096))
   261  	fmt.Fprintf(b, "{")
   262  	firstValue := true
   263  	for k, v := range m {
   264  		if firstValue {
   265  			firstValue = false
   266  		} else {
   267  			fmt.Fprintf(b, ", ")
   268  		}
   269  		fmt.Fprintf(b, "\"%v\": %v", k, strconv.Quote(v))
   270  	}
   271  	fmt.Fprintf(b, "}")
   272  	return b.String()
   273  }
   274  
   275  var (
   276  	varsMu             sync.Mutex
   277  	combinedDimensions map[string]bool
   278  	droppedVars        map[string]bool
   279  )
   280  
   281  // IsDimensionCombined returns true if the specified dimension should be combined.
   282  func IsDimensionCombined(name string) bool {
   283  	varsMu.Lock()
   284  	defer varsMu.Unlock()
   285  
   286  	if combinedDimensions == nil {
   287  		dims := strings.Split(combineDimensions, ",")
   288  		combinedDimensions = make(map[string]bool, len(dims))
   289  		for _, dim := range dims {
   290  			if dim == "" {
   291  				continue
   292  			}
   293  			combinedDimensions[dim] = true
   294  		}
   295  	}
   296  	return combinedDimensions[name]
   297  }
   298  
   299  // safeJoinLabels joins the label values with ".", but first replaces any existing
   300  // "." characters in the labels with the proper replacement, to avoid issues parsing
   301  // them apart later. The function also replaces specific label values with "all"
   302  // if a dimenstion is marked as true in combinedLabels.
   303  func safeJoinLabels(labels []string, combinedLabels []bool) string {
   304  	sanitizedLabels := make([]string, len(labels))
   305  	for idx, label := range labels {
   306  		if combinedLabels != nil && combinedLabels[idx] {
   307  			sanitizedLabels[idx] = StatsAllStr
   308  		} else {
   309  			sanitizedLabels[idx] = safeLabel(label)
   310  		}
   311  	}
   312  	return strings.Join(sanitizedLabels, ".")
   313  }
   314  
   315  func safeLabel(label string) string {
   316  	return strings.Replace(label, ".", "_", -1)
   317  }
   318  
   319  func isVarDropped(name string) bool {
   320  	varsMu.Lock()
   321  	defer varsMu.Unlock()
   322  
   323  	if droppedVars == nil {
   324  		dims := strings.Split(dropVariables, ",")
   325  		droppedVars = make(map[string]bool, len(dims))
   326  		for _, dim := range dims {
   327  			if dim == "" {
   328  				continue
   329  			}
   330  			droppedVars[dim] = true
   331  		}
   332  	}
   333  	return droppedVars[name]
   334  }
   335  
   336  // ParseCommonTags parses a comma-separated string into map of tags
   337  // If you want to global service values like host, service name, git revision, etc,
   338  // this is the place to do it.
   339  func ParseCommonTags(tagMapString []string) map[string]string {
   340  	tags := make(map[string]string)
   341  	for _, input := range tagMapString {
   342  		if strings.Contains(input, ":") {
   343  			tag := strings.Split(input, ":")
   344  			tags[strings.TrimSpace(tag[0])] = strings.TrimSpace(tag[1])
   345  		}
   346  	}
   347  	return tags
   348  }