560305f601
- uses newer version of go-ethereum required for go1.11
287 lines
6.3 KiB
Go
287 lines
6.3 KiB
Go
// Copyright (c) 2018 Arista Networks, Inc.
|
|
// Use of this source code is governed by the Apache License 2.0
|
|
// that can be found in the COPYING file.
|
|
|
|
// test2influxdb writes results from 'go test -json' to an influxdb
|
|
// database.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// go test -json | test2influxdb [options...]
|
|
//
|
|
// Points are written to influxdb with tags:
|
|
//
|
|
// package
|
|
// type "package" for a package result; "test" for a test result
|
|
// Additional tags set by -tags flag
|
|
//
|
|
// And fields:
|
|
//
|
|
// test string // "NONE" for whole package results
|
|
// elapsed float64 // in seconds
|
|
// pass float64 // 1 for PASS, 0 for FAIL
|
|
// Additional fields set by -fields flag
|
|
//
|
|
// "test" is a field instead of a tag to reduce cardinality of data.
|
|
//
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aristanetworks/glog"
|
|
client "github.com/influxdata/influxdb/client/v2"
|
|
)
|
|
|
|
type tag struct {
|
|
key string
|
|
value string
|
|
}
|
|
|
|
type tags []tag
|
|
|
|
func (ts *tags) String() string {
|
|
s := make([]string, len(*ts))
|
|
for i, t := range *ts {
|
|
s[i] = t.key + "=" + t.value
|
|
}
|
|
return strings.Join(s, ",")
|
|
}
|
|
|
|
func (ts *tags) Set(s string) error {
|
|
for _, fieldString := range strings.Split(s, ",") {
|
|
kv := strings.Split(fieldString, "=")
|
|
if len(kv) != 2 {
|
|
return fmt.Errorf("invalid tag, expecting one '=': %q", fieldString)
|
|
}
|
|
key := strings.TrimSpace(kv[0])
|
|
if key == "" {
|
|
return fmt.Errorf("invalid tag key %q in %q", key, fieldString)
|
|
}
|
|
val := strings.TrimSpace(kv[1])
|
|
if val == "" {
|
|
return fmt.Errorf("invalid tag value %q in %q", val, fieldString)
|
|
}
|
|
|
|
*ts = append(*ts, tag{key: key, value: val})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type field struct {
|
|
key string
|
|
value interface{}
|
|
}
|
|
|
|
type fields []field
|
|
|
|
func (fs *fields) String() string {
|
|
s := make([]string, len(*fs))
|
|
for i, f := range *fs {
|
|
var valString string
|
|
switch v := f.value.(type) {
|
|
case bool:
|
|
valString = strconv.FormatBool(v)
|
|
case float64:
|
|
valString = strconv.FormatFloat(v, 'f', -1, 64)
|
|
case int64:
|
|
valString = strconv.FormatInt(v, 10) + "i"
|
|
case string:
|
|
valString = v
|
|
}
|
|
|
|
s[i] = f.key + "=" + valString
|
|
}
|
|
return strings.Join(s, ",")
|
|
}
|
|
|
|
func (fs *fields) Set(s string) error {
|
|
for _, fieldString := range strings.Split(s, ",") {
|
|
kv := strings.Split(fieldString, "=")
|
|
if len(kv) != 2 {
|
|
return fmt.Errorf("invalid field, expecting one '=': %q", fieldString)
|
|
}
|
|
key := strings.TrimSpace(kv[0])
|
|
if key == "" {
|
|
return fmt.Errorf("invalid field key %q in %q", key, fieldString)
|
|
}
|
|
val := strings.TrimSpace(kv[1])
|
|
if val == "" {
|
|
return fmt.Errorf("invalid field value %q in %q", val, fieldString)
|
|
}
|
|
var value interface{}
|
|
var err error
|
|
if value, err = strconv.ParseBool(val); err == nil {
|
|
// It's a bool
|
|
} else if value, err = strconv.ParseFloat(val, 64); err == nil {
|
|
// It's a float64
|
|
} else if value, err = strconv.ParseInt(val[:len(val)-1], 0, 64); err == nil &&
|
|
val[len(val)-1] == 'i' {
|
|
// ints are suffixed with an "i"
|
|
} else {
|
|
value = val
|
|
}
|
|
|
|
*fs = append(*fs, field{key: key, value: value})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
flagAddr = flag.String("addr", "http://localhost:8086", "adddress of influxdb database")
|
|
flagDB = flag.String("db", "gotest", "use `database` in influxdb")
|
|
flagMeasurement = flag.String("m", "result", "`measurement` used in influxdb database")
|
|
|
|
flagTags tags
|
|
flagFields fields
|
|
)
|
|
|
|
func init() {
|
|
flag.Var(&flagTags, "tags", "set additional `tags`. Ex: name=alice,food=pasta")
|
|
flag.Var(&flagFields, "fields", "set additional `fields`. Ex: id=1234i,long=34.123,lat=72.234")
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
c, err := client.NewHTTPClient(client.HTTPConfig{
|
|
Addr: *flagAddr,
|
|
})
|
|
if err != nil {
|
|
glog.Fatal(err)
|
|
}
|
|
|
|
batch, err := client.NewBatchPoints(client.BatchPointsConfig{Database: *flagDB})
|
|
if err != nil {
|
|
glog.Fatal(err)
|
|
}
|
|
|
|
if err := parseTestOutput(os.Stdin, batch); err != nil {
|
|
glog.Fatal(err)
|
|
}
|
|
|
|
if err := c.Write(batch); err != nil {
|
|
glog.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// See https://golang.org/cmd/test2json/ for a description of 'go test
|
|
// -json' output
|
|
type testEvent struct {
|
|
Time time.Time // encodes as an RFC3339-format string
|
|
Action string
|
|
Package string
|
|
Test string
|
|
Elapsed float64 // seconds
|
|
Output string
|
|
}
|
|
|
|
func createTags(e *testEvent) map[string]string {
|
|
tags := make(map[string]string, len(flagTags)+2)
|
|
for _, t := range flagTags {
|
|
tags[t.key] = t.value
|
|
}
|
|
resultType := "test"
|
|
if e.Test == "" {
|
|
resultType = "package"
|
|
}
|
|
tags["package"] = e.Package
|
|
tags["type"] = resultType
|
|
return tags
|
|
}
|
|
|
|
func createFields(e *testEvent) map[string]interface{} {
|
|
fields := make(map[string]interface{}, len(flagFields)+3)
|
|
for _, f := range flagFields {
|
|
fields[f.key] = f.value
|
|
}
|
|
// Use a float64 instead of a bool to be able to SUM test
|
|
// successes in influxdb.
|
|
var pass float64
|
|
if e.Action == "pass" {
|
|
pass = 1
|
|
}
|
|
fields["pass"] = pass
|
|
fields["elapsed"] = e.Elapsed
|
|
if e.Test != "" {
|
|
fields["test"] = e.Test
|
|
}
|
|
return fields
|
|
}
|
|
|
|
func parseTestOutput(r io.Reader, batch client.BatchPoints) error {
|
|
// pkgs holds packages seen in r. Unfortunately, if a test panics,
|
|
// then there is no "fail" result from a package. To detect these
|
|
// kind of failures, keep track of all the packages that never had
|
|
// a "pass" or "fail".
|
|
//
|
|
// The last seen timestamp is stored with the package, so that
|
|
// package result measurement written to influxdb can be later
|
|
// than any test result for that package.
|
|
pkgs := make(map[string]time.Time)
|
|
d := json.NewDecoder(r)
|
|
for {
|
|
e := &testEvent{}
|
|
if err := d.Decode(e); err != nil {
|
|
if err != io.EOF {
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
|
|
switch e.Action {
|
|
case "pass", "fail":
|
|
default:
|
|
continue
|
|
}
|
|
|
|
if e.Test == "" {
|
|
// A package has completed.
|
|
delete(pkgs, e.Package)
|
|
} else {
|
|
pkgs[e.Package] = e.Time
|
|
}
|
|
|
|
point, err := client.NewPoint(
|
|
*flagMeasurement,
|
|
createTags(e),
|
|
createFields(e),
|
|
e.Time,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
batch.AddPoint(point)
|
|
}
|
|
|
|
for pkg, t := range pkgs {
|
|
pkgFail := &testEvent{
|
|
Action: "fail",
|
|
Package: pkg,
|
|
}
|
|
point, err := client.NewPoint(
|
|
*flagMeasurement,
|
|
createTags(pkgFail),
|
|
createFields(pkgFail),
|
|
// Fake a timestamp that is later than anything that
|
|
// occurred for this package
|
|
t.Add(time.Millisecond),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
batch.AddPoint(point)
|
|
}
|
|
|
|
return nil
|
|
}
|