Skip to content

Commit

Permalink
engine: report go version on startup
Browse files Browse the repository at this point in the history
The first time you report any metric, additionally report the running
Go version. Add two flags that can be used to disable this behavior.
  • Loading branch information
kevinburkesegment committed Jun 26, 2024
1 parent 59b8039 commit 9b59a64
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 22 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions datadog/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
43 changes: 39 additions & 4 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

// This cache keeps track of the generated measure structures to avoid
Expand All @@ -35,16 +38,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.
Expand Down Expand Up @@ -138,7 +144,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)

Expand Down
30 changes: 19 additions & 11 deletions engine_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package stats_test

import (
"io/ioutil"
"io"
"net/http"
"reflect"
"strings"
Expand Down Expand Up @@ -70,14 +70,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) {
Expand Down Expand Up @@ -342,7 +349,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
Expand Down Expand Up @@ -522,7 +530,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
}
6 changes: 6 additions & 0 deletions netstats/conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
35 changes: 28 additions & 7 deletions netstats/listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ package netstats
import (
"net"
"reflect"
"runtime"
"strings"
"testing"

"github.com/segmentio/stats/v4"
"github.com/segmentio/stats/v4/statstest"
)

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)

Expand Down Expand Up @@ -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())
Expand Down

0 comments on commit 9b59a64

Please sign in to comment.