main.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // Copyright (c) 2018 Arista Networks, Inc.
  2. // Use of this source code is governed by the Apache License 2.0
  3. // that can be found in the COPYING file.
  4. // test2influxdb writes results from 'go test -json' to an influxdb
  5. // database.
  6. //
  7. // Example usage:
  8. //
  9. // go test -json | test2influxdb [options...]
  10. //
  11. // Points are written to influxdb with tags:
  12. //
  13. // package
  14. // type "package" for a package result; "test" for a test result
  15. // Additional tags set by -tags flag
  16. //
  17. // And fields:
  18. //
  19. // test string // "NONE" for whole package results
  20. // elapsed float64 // in seconds
  21. // pass float64 // 1 for PASS, 0 for FAIL
  22. // Additional fields set by -fields flag
  23. //
  24. // "test" is a field instead of a tag to reduce cardinality of data.
  25. //
  26. package main
  27. import (
  28. "encoding/json"
  29. "flag"
  30. "fmt"
  31. "io"
  32. "os"
  33. "strconv"
  34. "strings"
  35. "time"
  36. "notabug.org/themusicgod1/glog"
  37. client "github.com/influxdata/influxdb/client/v2"
  38. )
  39. type tag struct {
  40. key string
  41. value string
  42. }
  43. type tags []tag
  44. func (ts *tags) String() string {
  45. s := make([]string, len(*ts))
  46. for i, t := range *ts {
  47. s[i] = t.key + "=" + t.value
  48. }
  49. return strings.Join(s, ",")
  50. }
  51. func (ts *tags) Set(s string) error {
  52. for _, fieldString := range strings.Split(s, ",") {
  53. kv := strings.Split(fieldString, "=")
  54. if len(kv) != 2 {
  55. return fmt.Errorf("invalid tag, expecting one '=': %q", fieldString)
  56. }
  57. key := strings.TrimSpace(kv[0])
  58. if key == "" {
  59. return fmt.Errorf("invalid tag key %q in %q", key, fieldString)
  60. }
  61. val := strings.TrimSpace(kv[1])
  62. if val == "" {
  63. return fmt.Errorf("invalid tag value %q in %q", val, fieldString)
  64. }
  65. *ts = append(*ts, tag{key: key, value: val})
  66. }
  67. return nil
  68. }
  69. type field struct {
  70. key string
  71. value interface{}
  72. }
  73. type fields []field
  74. func (fs *fields) String() string {
  75. s := make([]string, len(*fs))
  76. for i, f := range *fs {
  77. var valString string
  78. switch v := f.value.(type) {
  79. case bool:
  80. valString = strconv.FormatBool(v)
  81. case float64:
  82. valString = strconv.FormatFloat(v, 'f', -1, 64)
  83. case int64:
  84. valString = strconv.FormatInt(v, 10) + "i"
  85. case string:
  86. valString = v
  87. }
  88. s[i] = f.key + "=" + valString
  89. }
  90. return strings.Join(s, ",")
  91. }
  92. func (fs *fields) Set(s string) error {
  93. for _, fieldString := range strings.Split(s, ",") {
  94. kv := strings.Split(fieldString, "=")
  95. if len(kv) != 2 {
  96. return fmt.Errorf("invalid field, expecting one '=': %q", fieldString)
  97. }
  98. key := strings.TrimSpace(kv[0])
  99. if key == "" {
  100. return fmt.Errorf("invalid field key %q in %q", key, fieldString)
  101. }
  102. val := strings.TrimSpace(kv[1])
  103. if val == "" {
  104. return fmt.Errorf("invalid field value %q in %q", val, fieldString)
  105. }
  106. var value interface{}
  107. var err error
  108. if value, err = strconv.ParseBool(val); err == nil {
  109. // It's a bool
  110. } else if value, err = strconv.ParseFloat(val, 64); err == nil {
  111. // It's a float64
  112. } else if value, err = strconv.ParseInt(val[:len(val)-1], 0, 64); err == nil &&
  113. val[len(val)-1] == 'i' {
  114. // ints are suffixed with an "i"
  115. } else {
  116. value = val
  117. }
  118. *fs = append(*fs, field{key: key, value: value})
  119. }
  120. return nil
  121. }
  122. var (
  123. flagAddr = flag.String("addr", "http://localhost:8086", "adddress of influxdb database")
  124. flagDB = flag.String("db", "gotest", "use `database` in influxdb")
  125. flagMeasurement = flag.String("m", "result", "`measurement` used in influxdb database")
  126. flagTags tags
  127. flagFields fields
  128. )
  129. func init() {
  130. flag.Var(&flagTags, "tags", "set additional `tags`. Ex: name=alice,food=pasta")
  131. flag.Var(&flagFields, "fields", "set additional `fields`. Ex: id=1234i,long=34.123,lat=72.234")
  132. }
  133. func main() {
  134. flag.Parse()
  135. c, err := client.NewHTTPClient(client.HTTPConfig{
  136. Addr: *flagAddr,
  137. })
  138. if err != nil {
  139. glog.Fatal(err)
  140. }
  141. batch, err := client.NewBatchPoints(client.BatchPointsConfig{Database: *flagDB})
  142. if err != nil {
  143. glog.Fatal(err)
  144. }
  145. if err := parseTestOutput(os.Stdin, batch); err != nil {
  146. glog.Fatal(err)
  147. }
  148. if err := c.Write(batch); err != nil {
  149. glog.Fatal(err)
  150. }
  151. }
  152. // See https://golang.org/cmd/test2json/ for a description of 'go test
  153. // -json' output
  154. type testEvent struct {
  155. Time time.Time // encodes as an RFC3339-format string
  156. Action string
  157. Package string
  158. Test string
  159. Elapsed float64 // seconds
  160. Output string
  161. }
  162. func createTags(e *testEvent) map[string]string {
  163. tags := make(map[string]string, len(flagTags)+2)
  164. for _, t := range flagTags {
  165. tags[t.key] = t.value
  166. }
  167. resultType := "test"
  168. if e.Test == "" {
  169. resultType = "package"
  170. }
  171. tags["package"] = e.Package
  172. tags["type"] = resultType
  173. return tags
  174. }
  175. func createFields(e *testEvent) map[string]interface{} {
  176. fields := make(map[string]interface{}, len(flagFields)+3)
  177. for _, f := range flagFields {
  178. fields[f.key] = f.value
  179. }
  180. // Use a float64 instead of a bool to be able to SUM test
  181. // successes in influxdb.
  182. var pass float64
  183. if e.Action == "pass" {
  184. pass = 1
  185. }
  186. fields["pass"] = pass
  187. fields["elapsed"] = e.Elapsed
  188. if e.Test != "" {
  189. fields["test"] = e.Test
  190. }
  191. return fields
  192. }
  193. func parseTestOutput(r io.Reader, batch client.BatchPoints) error {
  194. // pkgs holds packages seen in r. Unfortunately, if a test panics,
  195. // then there is no "fail" result from a package. To detect these
  196. // kind of failures, keep track of all the packages that never had
  197. // a "pass" or "fail".
  198. //
  199. // The last seen timestamp is stored with the package, so that
  200. // package result measurement written to influxdb can be later
  201. // than any test result for that package.
  202. pkgs := make(map[string]time.Time)
  203. d := json.NewDecoder(r)
  204. for {
  205. e := &testEvent{}
  206. if err := d.Decode(e); err != nil {
  207. if err != io.EOF {
  208. return err
  209. }
  210. break
  211. }
  212. switch e.Action {
  213. case "pass", "fail":
  214. default:
  215. continue
  216. }
  217. if e.Test == "" {
  218. // A package has completed.
  219. delete(pkgs, e.Package)
  220. } else {
  221. pkgs[e.Package] = e.Time
  222. }
  223. point, err := client.NewPoint(
  224. *flagMeasurement,
  225. createTags(e),
  226. createFields(e),
  227. e.Time,
  228. )
  229. if err != nil {
  230. return err
  231. }
  232. batch.AddPoint(point)
  233. }
  234. for pkg, t := range pkgs {
  235. pkgFail := &testEvent{
  236. Action: "fail",
  237. Package: pkg,
  238. }
  239. point, err := client.NewPoint(
  240. *flagMeasurement,
  241. createTags(pkgFail),
  242. createFields(pkgFail),
  243. // Fake a timestamp that is later than anything that
  244. // occurred for this package
  245. t.Add(time.Millisecond),
  246. )
  247. if err != nil {
  248. return err
  249. }
  250. batch.AddPoint(point)
  251. }
  252. return nil
  253. }