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 }