diff --git a/plugins/runtime/doc.go b/plugins/runtime/doc.go new file mode 100644 index 00000000000..f534cf9e104 --- /dev/null +++ b/plugins/runtime/doc.go @@ -0,0 +1,18 @@ +// package runtime implements the work-in-progress conventional runtime metrics specified by OpenTelemetry. +// +// The metrics produced are: +// runtime.go.cgo.calls - Number of cgo calls made by the current process +// runtime.go.gc.count - Number of completed garbage collection cycles +// runtime.go.gc.pause_ns (ns) Amount of nanoseconds in GC stop-the-world pauses +// runtime.go.gc.pause_total_ns (ns) Cumulative nanoseconds in GC stop-the-world pauses since the program started +// runtime.go.goroutines - Number of goroutines that currently exist +// runtime.go.lookups - Number of pointer lookups performed by the runtime +// runtime.go.mem.heap_alloc (bytes) Bytes of allocated heap objects +// runtime.go.mem.heap_idle (bytes) Bytes in idle (unused) spans +// runtime.go.mem.heap_inuse (bytes) Bytes in in-use spans +// runtime.go.mem.heap_objects - Number of allocated heap objects +// runtime.go.mem.heap_released (bytes) Bytes of idle spans whose physical memory has been returned to the OS +// runtime.go.mem.heap_sys (bytes) Bytes of heap memory obtained from the OS +// runtime.go.mem.live_objects - Number of live objects is the number of cumulative Mallocs - Frees +// runtime.uptime (ms) Milliseconds since application was initialized +package runtime diff --git a/plugins/runtime/example/main.go b/plugins/runtime/example/main.go new file mode 100644 index 00000000000..75cb5f26dfd --- /dev/null +++ b/plugins/runtime/example/main.go @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + "time" + + "go.opentelemetry.io/otel/api/global" + metricstdout "go.opentelemetry.io/otel/exporters/metric/stdout" + "go.opentelemetry.io/otel/sdk/metric/controller/push" + + "go.opentelemetry.io/contrib/plugins/runtime" +) + +func initMeter() *push.Controller { + pusher, err := metricstdout.NewExportPipeline(metricstdout.Config{ + Quantiles: []float64{0.5}, + PrettyPrint: true, + }, 10*time.Second) + if err != nil { + log.Panicf("failed to initialize metric stdout exporter %v", err) + } + global.SetMeterProvider(pusher) + return pusher +} + +func main() { + defer initMeter().Stop() + + meter := global.Meter("runtime") + + r := runtime.New(meter, time.Second) + err := r.Start() + if err != nil { + panic(err) + } + + stopChan := make(chan os.Signal, 1) + signal.Notify(stopChan, syscall.SIGTERM, syscall.SIGINT) + <-stopChan + + r.Stop() +} diff --git a/plugins/runtime/go.mod b/plugins/runtime/go.mod new file mode 100644 index 00000000000..51715fc4210 --- /dev/null +++ b/plugins/runtime/go.mod @@ -0,0 +1,8 @@ +module go.opentelemetry.io/contrib/plugins/runtime + +go 1.14 + +require ( + github.com/stretchr/testify v1.4.0 + go.opentelemetry.io/otel v0.4.3 +) diff --git a/plugins/runtime/go.sum b/plugins/runtime/go.sum new file mode 100644 index 00000000000..e4d73ffcb24 --- /dev/null +++ b/plugins/runtime/go.sum @@ -0,0 +1,77 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs= +github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60= +github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w= +github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.opentelemetry.io/otel v0.4.3 h1:CroUX/0O1ZDcF0iWOO8gwYFWb5EbdSF0/C1yosO+Vhs= +go.opentelemetry.io/otel v0.4.3/go.mod h1:jzBIgIzK43Iu1BpDAXwqOd6UPsSAk+ewVZ5ofSXw4Ek= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/plugins/runtime/runtime.go b/plugins/runtime/runtime.go new file mode 100644 index 00000000000..c2dbb06b405 --- /dev/null +++ b/plugins/runtime/runtime.go @@ -0,0 +1,313 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime // import "go.opentelemetry.io/contrib/plugins/runtime" + +import ( + "context" + "errors" + goruntime "runtime" + "sync" + "time" + + "go.opentelemetry.io/otel/api/metric" + "go.opentelemetry.io/otel/api/unit" +) + +// Runtime reports the work-in-progress conventional runtime metrics specified by OpenTelemetry +type Runtime struct { + mu sync.RWMutex + meter metric.Meter + interval time.Duration + done chan bool + + metrics struct { + goCgoCalls metric.Int64Counter + goLookups metric.Int64Counter + goGcCount metric.Int64Counter + gcPauseNs metric.Int64Measure + } + + memStats goruntime.MemStats + numCgoCalls int64 +} + +// New returns Runtime, a structure for reporting Go runtime metrics +// interval is used to define how often to invoke Go runtime.ReadMemStats() to obtain metric data. It should be noted +// this package invokes a stop-the-world function on this interval. The interval should not be set arbitrarily small +// without accepting the performance overhead. +// TODO this interval may be removed in favor of otel SDK control after batch observers land +func New(meter metric.Meter, interval time.Duration) *Runtime { + r := &Runtime{ + meter: meter, + interval: interval, + done: make(chan bool), + } + + return r +} + +// Start begins regular background polling of Go runtime metrics and will return an error if any issues are encountered +func (r *Runtime) Start() error { + if r.interval <= 0 { + return errors.New("non-positive interval for runtime.New") + } + + err := r.register() + if err != nil { + return err + } + + go r.ticker() + + return nil +} + +// Stop terminates the regular background polling of Go runtime metrics +func (r *Runtime) Stop() { + r.done <- true +} + +func (r *Runtime) ticker() { + ticker := time.NewTicker(r.interval) + defer ticker.Stop() + + for { + select { + case <-r.done: + return + case <-ticker.C: + ctx := context.Background() + r.collect(ctx) + } + } +} + +func (r *Runtime) register() error { + r.mu.Lock() + defer r.mu.Unlock() + + var err error + + t0 := time.Now() + _, err = r.meter.RegisterInt64Observer("runtime.uptime", + func(result metric.Int64ObserverResult) { + result.Observe(time.Since(t0).Milliseconds()) + }, + metric.WithUnit(unit.Milliseconds), + metric.WithDescription("Milliseconds since application was initialized"), + ) + if err != nil { + return err + } + + _, err = r.meter.RegisterInt64Observer("runtime.go.goroutines", func(result metric.Int64ObserverResult) { + result.Observe(int64(goruntime.NumGoroutine())) + }, metric.WithDescription("Number of goroutines that currently exist")) + if err != nil { + return err + } + + r.metrics.goCgoCalls, err = r.meter.NewInt64Counter("runtime.go.cgo.calls", + metric.WithDescription("Number of cgo calls made by the current process")) + if err != nil { + return err + } + + // poll now so that the first tick has a full delta + r.numCgoCalls = goruntime.NumCgoCall() + goruntime.ReadMemStats(&r.memStats) + + err = r.registerMemStats() + if err != nil { + return err + } + + err = r.registerGcStats() + if err != nil { + return err + } + + // TODO go version as tag + + return nil +} + +func (r *Runtime) registerMemStats() error { + var err error + + _, err = r.meter.RegisterInt64Observer("runtime.go.mem.heap_alloc", func(result metric.Int64ObserverResult) { + r.mu.RLock() + defer r.mu.RUnlock() + result.Observe(int64(r.memStats.HeapAlloc)) + }, metric.WithUnit(unit.Bytes), metric.WithDescription("Bytes of allocated heap objects")) + if err != nil { + return err + } + + _, err = r.meter.RegisterInt64Observer("runtime.go.mem.heap_idle", func(result metric.Int64ObserverResult) { + r.mu.RLock() + defer r.mu.RUnlock() + result.Observe(int64(r.memStats.HeapIdle)) + }, metric.WithUnit(unit.Bytes), metric.WithDescription("Bytes in idle (unused) spans")) + if err != nil { + return err + } + + _, err = r.meter.RegisterInt64Observer("runtime.go.mem.heap_inuse", func(result metric.Int64ObserverResult) { + r.mu.RLock() + defer r.mu.RUnlock() + result.Observe(int64(r.memStats.HeapInuse)) + }, metric.WithUnit(unit.Bytes), metric.WithDescription("Bytes in in-use spans")) + if err != nil { + return err + } + + _, err = r.meter.RegisterInt64Observer("runtime.go.mem.heap_objects", func(result metric.Int64ObserverResult) { + r.mu.RLock() + defer r.mu.RUnlock() + result.Observe(int64(r.memStats.HeapObjects)) + }, metric.WithDescription("Number of allocated heap objects")) + if err != nil { + return err + } + + // https://github.com/golang/go/issues/32284 is actually gauge + _, err = r.meter.RegisterInt64Observer("runtime.go.mem.heap_released", func(result metric.Int64ObserverResult) { + r.mu.RLock() + defer r.mu.RUnlock() + result.Observe(int64(r.memStats.HeapReleased)) + }, metric.WithUnit(unit.Bytes), + metric.WithDescription("Bytes of idle spans whose physical memory has been returned to the OS")) + if err != nil { + return err + } + + _, err = r.meter.RegisterInt64Observer("runtime.go.mem.heap_sys", func(result metric.Int64ObserverResult) { + r.mu.RLock() + defer r.mu.RUnlock() + result.Observe(int64(r.memStats.HeapSys)) + }, metric.WithUnit(unit.Bytes), metric.WithDescription("Bytes of heap memory obtained from the OS")) + if err != nil { + return err + } + + r.metrics.goLookups, err = r.meter.NewInt64Counter("runtime.go.lookups", + metric.WithDescription("Number of pointer lookups performed by the runtime")) + if err != nil { + return err + } + + _, err = r.meter.RegisterInt64Observer("runtime.go.mem.live_objects", func(result metric.Int64ObserverResult) { + r.mu.RLock() + defer r.mu.RUnlock() + result.Observe(int64(r.memStats.Mallocs - r.memStats.Frees)) + }, metric.WithDescription("Number of live objects is the number of cumulative Mallocs - Frees")) + if err != nil { + return err + } + + return err +} + +func (r *Runtime) registerGcStats() error { + var err error + + r.metrics.goGcCount, err = r.meter.NewInt64Counter("runtime.go.gc.count", + metric.WithDescription("Number of completed garbage collection cycles")) + if err != nil { + return err + } + + _, err = r.meter.RegisterInt64Observer("runtime.go.gc.pause_total_ns", func(result metric.Int64ObserverResult) { + r.mu.RLock() + defer r.mu.RUnlock() + result.Observe(int64(r.memStats.PauseTotalNs)) + }, metric.WithDescription("Cumulative nanoseconds in GC stop-the-world pauses since the program started")) + if err != nil { + return err + } + + r.metrics.gcPauseNs, err = r.meter.NewInt64Measure("runtime.go.gc.pause_ns", + metric.WithDescription("Amount of nanoseconds in GC stop-the-world pauses")) + if err != nil { + return err + } + + return nil +} + +func (r *Runtime) collect(ctx context.Context) { + r.mu.Lock() + defer r.mu.Unlock() + + lastNumCgoCalls := r.numCgoCalls + r.numCgoCalls = goruntime.NumCgoCall() + r.metrics.goCgoCalls.Add(ctx, r.numCgoCalls-lastNumCgoCalls) + + lastLookups := r.memStats.Lookups + lastNumGC := r.memStats.NumGC + + pauses := collectMemoryStats(&r.memStats, lastNumGC) + + r.metrics.goLookups.Add(ctx, int64(r.memStats.Lookups-lastLookups)) + r.metrics.goGcCount.Add(ctx, int64(r.memStats.NumGC-lastNumGC)) + + for _, pause := range pauses { + r.metrics.gcPauseNs.Record(ctx, pause.Nanoseconds()) + } +} + +func collectMemoryStats(memStats *goruntime.MemStats, lastNumGC uint32) (pauses []time.Duration) { + goruntime.ReadMemStats(memStats) + return makeGCPauses(memStats, lastNumGC) +} + +func makeGCPauses(memStats *goruntime.MemStats, lastNumGC uint32) (pauses []time.Duration) { + delta := int(memStats.NumGC - lastNumGC) + + if delta == 0 { + return nil + } + + if delta >= len(memStats.PauseNs) { + return makePauses(memStats.PauseNs[:], nil) + } + + length := uint32(len(memStats.PauseNs)) + offset := length - 1 + + i := (lastNumGC + offset + 1) % length + j := (memStats.NumGC + offset + 1) % length + + if j < i { // wrap around the circular buffer + return makePauses(memStats.PauseNs[i:], memStats.PauseNs[:j]) + } + + return makePauses(memStats.PauseNs[i:j], nil) +} + +func makePauses(head []uint64, tail []uint64) (pauses []time.Duration) { + pauses = make([]time.Duration, 0, len(head)+len(tail)) + pauses = appendPauses(pauses, head) + pauses = appendPauses(pauses, tail) + return +} + +func appendPauses(pauses []time.Duration, values []uint64) []time.Duration { + for _, v := range values { + pauses = append(pauses, time.Duration(v)) + } + return pauses +} diff --git a/plugins/runtime/runtime_test.go b/plugins/runtime/runtime_test.go new file mode 100644 index 00000000000..95051b3f84c --- /dev/null +++ b/plugins/runtime/runtime_test.go @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/api/global" +) + +func TestRuntime(t *testing.T) { + meter := global.Meter("test") + r := New(meter, time.Second) + err := r.Start() + assert.NoError(t, err) + time.Sleep(time.Second) + r.Stop() +}