export.go 8.8 KB

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