diff --git a/README.md b/README.md index 00c28f8..2fcc060 100644 --- a/README.md +++ b/README.md @@ -385,3 +385,16 @@ func main() { // ... } ``` + +### Addendum + +By default, the stats library will report the running go version when you +invoke NewEngine() as three metrics: + +- `go_version.major` +- `go_version.minor` +- `go_version.patch` + +Set `STATS_DISABLE_GO_VERSION_REPORTING` to `true` in your environment, or set +`stats.GoVersionReportingEnabled` to `false` before collecting any metrics, to +disable this behavior. diff --git a/datadog/server_test.go b/datadog/server_test.go index 31b9eb8..b9bd32c 100644 --- a/datadog/server_test.go +++ b/datadog/server_test.go @@ -13,6 +13,9 @@ import ( ) func TestServer(t *testing.T) { + initValue := stats.GoVersionReportingEnabled + stats.GoVersionReportingEnabled = false + defer func() { stats.GoVersionReportingEnabled = initValue }() engine := stats.NewEngine("datadog.test", nil) a := uint32(0) @@ -95,6 +98,7 @@ func TestServer(t *testing.T) { } func startTestServer(t *testing.T, handler Handler) (addr string, closer io.Closer) { + t.Helper() conn, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { t.Error(err) diff --git a/engine.go b/engine.go index 7f19d83..50eafc4 100644 --- a/engine.go +++ b/engine.go @@ -4,16 +4,19 @@ import ( "os" "path/filepath" "reflect" + "runtime" + "strconv" + "strings" "sync" "time" ) -// An Engine carries the context for producing metrics, it is configured by +// An Engine carries the context for producing metrics. It is configured by // setting the exported fields or using the helper methods to create sub-engines // that inherit the configuration of the base they were created from. // // The program must not modify the engine's handler, prefix, or tags after it -// started using it. If changes need to be made new engines must be created by +// starts using them. If changes need to be made new engines must be created by // calls to WithPrefix or WithTags. type Engine struct { // The measure handler that the engine forwards measures to. @@ -26,7 +29,7 @@ type Engine struct { // // The list of tags has to be sorted. This is automatically managed by the // helper methods WithPrefix, WithTags and the NewEngine function. A program - // that manipulates this field directly has to respect this requirement. + // that manipulates this field directly must respect this requirement. Tags []Tag // Indicates whether to allow duplicated tags from the tags list before sending. @@ -41,16 +44,19 @@ type Engine struct { // The cached values include the engine prefix in the measure names, which // is why the cache must be local to the engine. cache measureCache + + once sync.Once } // NewEngine creates and returns a new engine configured with prefix, handler, // and tags. func NewEngine(prefix string, handler Handler, tags ...Tag) *Engine { - return &Engine{ + e := &Engine{ Handler: handler, Prefix: prefix, Tags: SortTags(copyTags(tags)), } + return e } // Register adds handler to eng. @@ -144,7 +150,36 @@ func (eng *Engine) ClockAt(name string, start time.Time, tags ...Tag) *Clock { } } +var GoVersionReportingEnabled = os.Getenv("STATS_DISABLE_GO_VERSION_REPORTING") != "true" + func (eng *Engine) measure(t time.Time, name string, value interface{}, ftype FieldType, tags ...Tag) { + if GoVersionReportingEnabled { + eng.once.Do(func() { + vsn := strings.TrimPrefix(runtime.Version(), "go") + parts := strings.Split(vsn, ".") + // older Go version might be "go1.13" + if len(parts) == 2 || len(parts) == 3 { + maj, err := strconv.Atoi(parts[0]) + if err == nil { + eng.measureOne(t, "go_version.major", maj, Gauge) + } + min, err := strconv.Atoi(parts[1]) + if err == nil { + eng.measureOne(t, "go_version.minor", min, Gauge) + } + if len(parts) == 3 { + patch, err := strconv.Atoi(parts[2]) + if err == nil { + eng.measureOne(t, "go_version.patch", patch, Gauge) + } + } + } + }) + } + eng.measureOne(t, name, value, ftype, tags...) +} + +func (eng *Engine) measureOne(t time.Time, name string, value interface{}, ftype FieldType, tags ...Tag) { name, field := splitMeasureField(name) mp := measureArrayPool.Get().(*[1]Measure) diff --git a/engine_test.go b/engine_test.go index 5d0c678..282cac1 100644 --- a/engine_test.go +++ b/engine_test.go @@ -1,7 +1,7 @@ package stats_test import ( - "io/ioutil" + "io" "net/http" "reflect" "strings" @@ -74,14 +74,21 @@ func TestEngine(t *testing.T) { }, } - for _, test := range tests { - testFunc := test.function - t.Run(test.scenario, func(t *testing.T) { - t.Parallel() - h := &statstest.Handler{} - testFunc(t, stats.NewEngine("test", h, stats.T("service", "test-service"))) - }) - } + initValue := stats.GoVersionReportingEnabled + stats.GoVersionReportingEnabled = false + defer func() { stats.GoVersionReportingEnabled = initValue }() + // Extra t.Run is necessary so above defer runs after parallel tests + // complete. + t.Run("subtests", func(t *testing.T) { + for _, test := range tests { + testFunc := test.function + t.Run(test.scenario, func(t *testing.T) { + t.Parallel() + h := &statstest.Handler{} + testFunc(t, stats.NewEngine("test", h, stats.T("service", "test-service"))) + }) + } + }) } func testEngineWithPrefix(t *testing.T, eng *stats.Engine) { @@ -369,7 +376,8 @@ func BenchmarkEngine(b *testing.B) { }, } - for _, eng := range engines { + for i := range engines { + eng := &engines[i] b.Run(eng.name, func(b *testing.B) { tests := []struct { scenario string @@ -549,7 +557,7 @@ type discardTransport struct{} func (t *discardTransport) RoundTrip(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, - Body: ioutil.NopCloser(strings.NewReader("")), + Body: io.NopCloser(strings.NewReader("")), Request: req, }, nil } diff --git a/netstats/conn_test.go b/netstats/conn_test.go index 667f8c5..6639ee2 100644 --- a/netstats/conn_test.go +++ b/netstats/conn_test.go @@ -23,6 +23,9 @@ func TestBaseConn(t *testing.T) { } func TestConn(t *testing.T) { + initValue := stats.GoVersionReportingEnabled + stats.GoVersionReportingEnabled = false + defer func() { stats.GoVersionReportingEnabled = initValue }() h := &statstest.Handler{} e := stats.NewEngine("netstats.test", h) @@ -84,6 +87,9 @@ func TestConn(t *testing.T) { } func TestConnError(t *testing.T) { + initValue := stats.GoVersionReportingEnabled + stats.GoVersionReportingEnabled = false + defer func() { stats.GoVersionReportingEnabled = initValue }() h := &statstest.Handler{} e := stats.NewEngine("netstats.test", h) diff --git a/netstats/listener_test.go b/netstats/listener_test.go index 5f9c3a6..ba8881e 100644 --- a/netstats/listener_test.go +++ b/netstats/listener_test.go @@ -3,6 +3,8 @@ package netstats import ( "net" "reflect" + "runtime" + "strings" "testing" "github.com/segmentio/stats/v4" @@ -10,6 +12,9 @@ import ( ) func TestListener(t *testing.T) { + initValue := stats.GoVersionReportingEnabled + stats.GoVersionReportingEnabled = false + defer func() { stats.GoVersionReportingEnabled = initValue }() h := &statstest.Handler{} e := stats.NewEngine("netstats.test", h) @@ -58,15 +63,31 @@ func TestListenerError(t *testing.T) { lstn.Close() - expected := []stats.Measure{ - { - Name: "netstats.test.conn.error", - Fields: []stats.Field{stats.MakeField("count", 1, stats.Counter)}, - Tags: []stats.Tag{stats.T("operation", "accept"), stats.T("protocol", "tcp")}, - }, + vsn := strings.TrimPrefix(runtime.Version(), "go") + parts := strings.Split(vsn, ".") + measures := h.Measures() + if len(parts) == 2 || len(parts) == 3 { + if len(measures) != 1+len(parts) { + t.Fatalf("expecting to get %d metrics, got back %d: %v", 1+len(parts), len(measures), measures) + } + } + var foundMetric stats.Measure + for i := range measures { + if measures[i].Name == "netstats.test.conn.error" { + foundMetric = measures[i] + break + } + } + if foundMetric.Name == "" { + t.Errorf("did not find netstats metric: %v", measures) } - if !reflect.DeepEqual(expected, h.Measures()) { + expected := stats.Measure{ + Name: "netstats.test.conn.error", + Fields: []stats.Field{stats.MakeField("count", 1, stats.Counter)}, + Tags: []stats.Tag{stats.T("operation", "accept"), stats.T("protocol", "tcp")}, + } + if !reflect.DeepEqual(expected, foundMetric) { t.Error("bad measures:") t.Logf("expected: %v", expected) t.Logf("found: %v", h.Measures())