├── .travis.yml ├── .gitignore ├── doc.go ├── nut.json ├── http_status_metrics.go ├── examples ├── example1.go └── example_web.go ├── LICENSE ├── gc_metrics.go ├── timer_metrics.go ├── tracer_metrics_test.go ├── gometrica.go ├── memory_metrics.go ├── tracer_metrics.go ├── http_metrics.go ├── runtime_metrics.go ├── README.md └── agent.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.nut 2 | *.swp 3 | examples/example1 4 | examples/example_web 5 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package gorelic is an New Relic agent implementation for Go runtime. It collect a lot of metrics about Go scheduler, garbage collector and memory allocator and send them to NewRelic. 2 | package gorelic 3 | -------------------------------------------------------------------------------- /nut.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "0.0.6", 3 | "Vendor": "yvasiyarov", 4 | "Authors": [ 5 | { 6 | "FullName": "Yuriy Vasiyarov", 7 | "Email": "varyous@gmail.com" 8 | } 9 | ], 10 | "ExtraFiles": [ 11 | "README.md", 12 | "LICENSE" 13 | ], 14 | "Homepage": "https://github.com/yvasiyarov/gorelic" 15 | } 16 | -------------------------------------------------------------------------------- /http_status_metrics.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yvasiyarov/go-metrics" 7 | "github.com/yvasiyarov/newrelic_platform_go" 8 | ) 9 | 10 | // New metrica collector - counter per each http status code. 11 | type counterByStatusMetrica struct { 12 | counter metrics.Counter 13 | name string 14 | units string 15 | } 16 | 17 | // GetName: metrics.IMetrica interface implementation. 18 | func (m *counterByStatusMetrica) GetName() string { return m.name } 19 | 20 | func (m *counterByStatusMetrica) GetUnits() string { return m.units } 21 | 22 | func (m *counterByStatusMetrica) GetValue() (float64, error) { return float64(m.counter.Count()), nil } 23 | 24 | // addHTTPStatusMetricsToComponent initializes counter metrics for all http statuses and adds them to the component. 25 | func addHTTPStatusMetricsToComponent(component newrelic_platform_go.IComponent, statusCounters map[int]metrics.Counter) { 26 | for statusCode, counter := range statusCounters { 27 | component.AddMetrica(&counterByStatusMetrica{ 28 | counter: counter, 29 | name: fmt.Sprintf("http/status/%d", statusCode), 30 | units: "count", 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/example1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/yvasiyarov/gorelic" 6 | "log" 7 | "math/rand" 8 | "runtime" 9 | "time" 10 | ) 11 | 12 | var newrelicLicense = flag.String("newrelic-license", "", "Newrelic license") 13 | 14 | func allocateAndSum(arraySize int) int { 15 | arr := make([]int, arraySize, arraySize) 16 | for i := range arr { 17 | arr[i] = rand.Int() 18 | } 19 | time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond) 20 | 21 | result := 0 22 | for _, v := range arr { 23 | result += v 24 | } 25 | //log.Printf("Array size is: %d, sum is: %d\n", arraySize, result) 26 | return result 27 | } 28 | 29 | func doSomeJob(numRoutines int) { 30 | for { 31 | for i := 0; i < numRoutines; i++ { 32 | go allocateAndSum(rand.Intn(1024) * 1024) 33 | } 34 | log.Printf("All %d routines started\n", numRoutines) 35 | time.Sleep(1000 * time.Millisecond) 36 | runtime.GC() 37 | } 38 | } 39 | 40 | func main() { 41 | 42 | flag.Parse() 43 | if *newrelicLicense == "" { 44 | log.Fatalf("Please, pass a valid newrelic license key.\n Use --help to get more information about available options\n") 45 | } 46 | agent := gorelic.NewAgent() 47 | agent.Verbose = true 48 | agent.NewrelicLicense = *newrelicLicense 49 | agent.Run() 50 | 51 | doSomeJob(100) 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Yuriy Vasiyarov. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /gc_metrics.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | metrics "github.com/yvasiyarov/go-metrics" 5 | "github.com/yvasiyarov/newrelic_platform_go" 6 | "time" 7 | ) 8 | 9 | func newGCMetricaDataSource(pollInterval int) goMetricaDataSource { 10 | r := metrics.NewRegistry() 11 | 12 | metrics.RegisterDebugGCStats(r) 13 | go metrics.CaptureDebugGCStats(r, time.Duration(pollInterval)*time.Second) 14 | return goMetricaDataSource{r} 15 | } 16 | 17 | func addGCMetricsToComponent(component newrelic_platform_go.IComponent, pollInterval int) { 18 | metrics := []*baseGoMetrica{ 19 | &baseGoMetrica{ 20 | name: "NumberOfGCCalls", 21 | units: "calls", 22 | dataSourceKey: "debug.GCStats.NumGC", 23 | }, 24 | &baseGoMetrica{ 25 | name: "PauseTotalTime", 26 | units: "nanoseconds", 27 | dataSourceKey: "debug.GCStats.PauseTotal", 28 | }, 29 | } 30 | 31 | ds := newGCMetricaDataSource(pollInterval) 32 | for _, m := range metrics { 33 | m.basePath = "Runtime/GC/" 34 | m.dataSource = ds 35 | component.AddMetrica(&gaugeMetrica{m}) 36 | } 37 | 38 | histogramMetrics := []*histogramMetrica{ 39 | &histogramMetrica{ 40 | statFunction: histogramMax, 41 | baseGoMetrica: &baseGoMetrica{name: "Max"}, 42 | }, 43 | &histogramMetrica{ 44 | statFunction: histogramMin, 45 | baseGoMetrica: &baseGoMetrica{name: "Min"}, 46 | }, 47 | &histogramMetrica{ 48 | statFunction: histogramMean, 49 | baseGoMetrica: &baseGoMetrica{name: "Mean"}, 50 | }, 51 | &histogramMetrica{ 52 | statFunction: histogramPercentile, 53 | percentileValue: 0.95, 54 | baseGoMetrica: &baseGoMetrica{name: "Percentile95"}, 55 | }, 56 | } 57 | for _, m := range histogramMetrics { 58 | m.baseGoMetrica.units = "nanoseconds" 59 | m.baseGoMetrica.dataSourceKey = "debug.GCStats.Pause" 60 | m.baseGoMetrica.basePath = "Runtime/GC/GCTime/" 61 | m.baseGoMetrica.dataSource = ds 62 | 63 | component.AddMetrica(m) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /timer_metrics.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | metrics "github.com/yvasiyarov/go-metrics" 5 | "time" 6 | ) 7 | 8 | type baseTimerMetrica struct { 9 | dataSource metrics.Timer 10 | name string 11 | units string 12 | } 13 | 14 | func (metrica *baseTimerMetrica) GetName() string { 15 | return metrica.name 16 | } 17 | 18 | func (metrica *baseTimerMetrica) GetUnits() string { 19 | return metrica.units 20 | } 21 | 22 | type timerRate1Metrica struct { 23 | *baseTimerMetrica 24 | } 25 | 26 | func (metrica *timerRate1Metrica) GetValue() (float64, error) { 27 | return metrica.dataSource.Rate1(), nil 28 | } 29 | 30 | type timerRateMeanMetrica struct { 31 | *baseTimerMetrica 32 | } 33 | 34 | func (metrica *timerRateMeanMetrica) GetValue() (float64, error) { 35 | return metrica.dataSource.RateMean(), nil 36 | } 37 | 38 | type timerMeanMetrica struct { 39 | *baseTimerMetrica 40 | } 41 | 42 | func (metrica *timerMeanMetrica) GetValue() (float64, error) { 43 | return metrica.dataSource.Mean() / float64(time.Millisecond), nil 44 | } 45 | 46 | type timerMinMetrica struct { 47 | *baseTimerMetrica 48 | } 49 | 50 | func (metrica *timerMinMetrica) GetValue() (float64, error) { 51 | return float64(metrica.dataSource.Min()) / float64(time.Millisecond), nil 52 | } 53 | 54 | type timerMaxMetrica struct { 55 | *baseTimerMetrica 56 | } 57 | 58 | func (metrica *timerMaxMetrica) GetValue() (float64, error) { 59 | return float64(metrica.dataSource.Max()) / float64(time.Millisecond), nil 60 | } 61 | 62 | type timerPercentile75Metrica struct { 63 | *baseTimerMetrica 64 | } 65 | 66 | func (metrica *timerPercentile75Metrica) GetValue() (float64, error) { 67 | return metrica.dataSource.Percentile(0.75) / float64(time.Millisecond), nil 68 | } 69 | 70 | type timerPercentile90Metrica struct { 71 | *baseTimerMetrica 72 | } 73 | 74 | func (metrica *timerPercentile90Metrica) GetValue() (float64, error) { 75 | return metrica.dataSource.Percentile(0.90) / float64(time.Millisecond), nil 76 | } 77 | 78 | type timerPercentile95Metrica struct { 79 | *baseTimerMetrica 80 | } 81 | 82 | func (metrica *timerPercentile95Metrica) GetValue() (float64, error) { 83 | return metrica.dataSource.Percentile(0.95) / float64(time.Millisecond), nil 84 | } 85 | -------------------------------------------------------------------------------- /examples/example_web.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "expvar" 5 | "flag" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "net/http" 10 | "runtime" 11 | "time" 12 | 13 | "github.com/yvasiyarov/gorelic" 14 | ) 15 | 16 | var newrelicLicense = flag.String("newrelic-license", "", "Newrelic license") 17 | 18 | var numCalls = expvar.NewInt("num_calls") 19 | 20 | type WaveMetrica struct { 21 | sawtoothMax int 22 | sawtoothCounter int 23 | } 24 | 25 | func (metrica *WaveMetrica) GetName() string { 26 | return "Custom/Wave_Metrica" 27 | } 28 | func (metrica *WaveMetrica) GetUnits() string { 29 | return "Queries/Second" 30 | } 31 | func (metrica *WaveMetrica) GetValue() (float64, error) { 32 | metrica.sawtoothCounter++ 33 | if metrica.sawtoothCounter > metrica.sawtoothMax { 34 | metrica.sawtoothCounter = 0 35 | } 36 | return float64(metrica.sawtoothCounter), nil 37 | } 38 | 39 | func allocateAndSum(arraySize int) int { 40 | arr := make([]int, arraySize, arraySize) 41 | for i := range arr { 42 | arr[i] = rand.Int() 43 | } 44 | time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond) 45 | 46 | result := 0 47 | for _, v := range arr { 48 | result += v 49 | } 50 | //log.Printf("Array size is: %d, sum is: %d\n", arraySize, result) 51 | return result 52 | } 53 | 54 | func doSomeJob(numRoutines int) { 55 | for i := 0; i < numRoutines; i++ { 56 | go allocateAndSum(rand.Intn(1024) * 1024) 57 | } 58 | log.Printf("All %d routines started\n", numRoutines) 59 | time.Sleep(1000 * time.Millisecond) 60 | runtime.GC() 61 | } 62 | 63 | func helloServer(w http.ResponseWriter, req *http.Request) { 64 | doSomeJob(5) 65 | io.WriteString(w, "Did some work") 66 | } 67 | 68 | func main() { 69 | flag.Parse() 70 | if *newrelicLicense == "" { 71 | log.Fatalf("Please, pass a valid newrelic license key.\n Use --help to get more information about available options\n") 72 | } 73 | agent := gorelic.NewAgent() 74 | agent.Verbose = true 75 | agent.CollectHTTPStat = true 76 | agent.NewrelicLicense = *newrelicLicense 77 | agent.AddCustomMetric(&WaveMetrica{ 78 | sawtoothMax: 10, 79 | sawtoothCounter: 5, 80 | }) 81 | agent.Run() 82 | 83 | http.HandleFunc("/", agent.WrapHTTPHandlerFunc(helloServer)) 84 | http.ListenAndServe(":8080", nil) 85 | } 86 | -------------------------------------------------------------------------------- /tracer_metrics_test.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | "math/rand" 5 | "reflect" 6 | "sort" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/yvasiyarov/newrelic_platform_go" 12 | ) 13 | 14 | var ( 15 | dummyComponent = newrelic_platform_go.NewPluginComponent(DefaultAgentName, DefaultAgentGuid, false) 16 | ) 17 | 18 | func TestBeginEndTrace(t *testing.T) { 19 | tracer := newTracer(dummyComponent) 20 | 21 | trace := tracer.BeginTrace("dummy_trace") 22 | 23 | expectedTraceName := "Trace/dummy_trace" 24 | if trace.transaction.name != expectedTraceName { 25 | t.Errorf("Expected the trace name to be %s but got %s instead", expectedTraceName, trace.transaction.name) 26 | } 27 | 28 | expectedMetricaModelCount := 6 29 | if len(dummyComponent.MetricaModels) != expectedMetricaModelCount { 30 | t.Errorf("Expected the number of metrica models to be %d but got %d instead", expectedMetricaModelCount, len(dummyComponent.MetricaModels)) 31 | } 32 | 33 | var metricas []string 34 | for _, metrica := range dummyComponent.MetricaModels { 35 | metricas = append(metricas, metrica.GetName()) 36 | } 37 | sort.Strings(metricas) 38 | 39 | expectedMetricas := []string{"Trace/dummy_trace/max", "Trace/dummy_trace/mean", "Trace/dummy_trace/min", "Trace/dummy_trace/percentile75", "Trace/dummy_trace/percentile90", "Trace/dummy_trace/percentile95"} 40 | if !reflect.DeepEqual(metricas, expectedMetricas) { 41 | t.Errorf("Expected metricas to be %v buty got %v instead", metricas, expectedMetricas) 42 | } 43 | 44 | startTime := trace.transaction.timer.Count() 45 | trace.EndTrace() 46 | if trace.transaction.timer.Count() == startTime { 47 | t.Error("Expected the transaction timer to be incremented") 48 | } 49 | } 50 | 51 | func TestTrace(t *testing.T) { 52 | tracer := newTracer(dummyComponent) 53 | 54 | traceFuncExecuted := false 55 | dummyTraceFunc := func() { 56 | traceFuncExecuted = true 57 | } 58 | tracer.Trace("dummy_trace", dummyTraceFunc) 59 | 60 | if !traceFuncExecuted { 61 | t.Fatal("Trace func was not executed") 62 | } 63 | } 64 | 65 | func TestParallelTraces(t *testing.T) { 66 | tracer := newTracer(dummyComponent) 67 | metricNames := []string{"Leonardo", "Michalangelo", "Raphael", "Donatello"} 68 | goroutines := 32 69 | 70 | var wg sync.WaitGroup 71 | for i := 0; i < goroutines; i++ { 72 | wg.Add(1) 73 | nameIndex := i // Capture a copy of i 74 | go func() { 75 | trace := tracer.BeginTrace(metricNames[nameIndex%len(metricNames)]) 76 | defer trace.EndTrace() 77 | 78 | time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) 79 | 80 | wg.Done() 81 | }() 82 | } 83 | wg.Wait() 84 | 85 | if len(tracer.metrics) != len(metricNames) { 86 | t.Errorf("Expected to have %d metrics but got %d instead", len(metricNames), len(tracer.metrics)) 87 | } 88 | 89 | traces := int64(0) 90 | for _, metric := range tracer.metrics { 91 | traces += metric.timer.Count() 92 | } 93 | 94 | if traces != int64(goroutines) { 95 | t.Errorf("Expected to have %d traces but got %d instead", goroutines, traces) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /gometrica.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | "fmt" 5 | metrics "github.com/yvasiyarov/go-metrics" 6 | ) 7 | 8 | const ( 9 | histogramMin = iota 10 | histogramMax 11 | histogramMean 12 | histogramPercentile 13 | histogramStdDev 14 | histogramVariance 15 | noHistogramFunctions 16 | ) 17 | 18 | type goMetricaDataSource struct { 19 | metrics.Registry 20 | } 21 | 22 | func (ds goMetricaDataSource) GetGaugeValue(key string) (float64, error) { 23 | if valueContainer := ds.Get(key); valueContainer == nil { 24 | return 0, fmt.Errorf("metrica with name %s is not registered\n", key) 25 | } else if gauge, ok := valueContainer.(metrics.Gauge); ok { 26 | return float64(gauge.Value()), nil 27 | } else { 28 | return 0, fmt.Errorf("metrica container has unexpected type: %T\n", valueContainer) 29 | } 30 | } 31 | 32 | func (ds goMetricaDataSource) GetHistogramValue(key string, statFunction int, percentile float64) (float64, error) { 33 | if valueContainer := ds.Get(key); valueContainer == nil { 34 | return 0, fmt.Errorf("metrica with name %s is not registered\n", key) 35 | } else if histogram, ok := valueContainer.(metrics.Histogram); ok { 36 | switch statFunction { 37 | default: 38 | return 0, fmt.Errorf("unsupported stat function for histogram: %d\n", statFunction) 39 | case histogramMax: 40 | return float64(histogram.Max()), nil 41 | case histogramMin: 42 | return float64(histogram.Min()), nil 43 | case histogramMean: 44 | return float64(histogram.Mean()), nil 45 | case histogramStdDev: 46 | return float64(histogram.StdDev()), nil 47 | case histogramVariance: 48 | return float64(histogram.Variance()), nil 49 | case histogramPercentile: 50 | return float64(histogram.Percentile(percentile)), nil 51 | } 52 | } else { 53 | return 0, fmt.Errorf("metrica container has unexpected type: %T\n", valueContainer) 54 | } 55 | } 56 | 57 | type baseGoMetrica struct { 58 | dataSource goMetricaDataSource 59 | basePath string 60 | name string 61 | units string 62 | dataSourceKey string 63 | } 64 | 65 | func (metrica *baseGoMetrica) GetName() string { 66 | return metrica.basePath + metrica.name 67 | } 68 | 69 | func (metrica *baseGoMetrica) GetUnits() string { 70 | return metrica.units 71 | } 72 | 73 | type gaugeMetrica struct { 74 | *baseGoMetrica 75 | } 76 | 77 | func (metrica *gaugeMetrica) GetValue() (float64, error) { 78 | return metrica.dataSource.GetGaugeValue(metrica.dataSourceKey) 79 | } 80 | 81 | type gaugeIncMetrica struct { 82 | *baseGoMetrica 83 | previousValue float64 84 | } 85 | 86 | func (metrica *gaugeIncMetrica) GetValue() (float64, error) { 87 | var value float64 88 | var currentValue float64 89 | var err error 90 | if currentValue, err = metrica.dataSource.GetGaugeValue(metrica.dataSourceKey); err == nil { 91 | value = currentValue - metrica.previousValue 92 | metrica.previousValue = currentValue 93 | } 94 | return value, err 95 | } 96 | 97 | type histogramMetrica struct { 98 | *baseGoMetrica 99 | statFunction int 100 | percentileValue float64 101 | } 102 | 103 | func (metrica *histogramMetrica) GetValue() (float64, error) { 104 | return metrica.dataSource.GetHistogramValue(metrica.dataSourceKey, metrica.statFunction, metrica.percentileValue) 105 | } 106 | -------------------------------------------------------------------------------- /memory_metrics.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | metrics "github.com/yvasiyarov/go-metrics" 5 | "github.com/yvasiyarov/newrelic_platform_go" 6 | "time" 7 | ) 8 | 9 | func newMemoryMetricaDataSource(pollInterval int) goMetricaDataSource { 10 | r := metrics.NewRegistry() 11 | 12 | metrics.RegisterRuntimeMemStats(r) 13 | metrics.CaptureRuntimeMemStatsOnce(r) 14 | go metrics.CaptureRuntimeMemStats(r, time.Duration(pollInterval)*time.Second) 15 | return goMetricaDataSource{r} 16 | } 17 | 18 | func addMemoryMericsToComponent(component newrelic_platform_go.IComponent, pollInterval int) { 19 | gaugeMetrics := []*baseGoMetrica{ 20 | //Memory in use metrics 21 | &baseGoMetrica{ 22 | name: "InUse/Total", 23 | units: "bytes", 24 | dataSourceKey: "runtime.MemStats.Alloc", 25 | }, 26 | &baseGoMetrica{ 27 | name: "InUse/Heap", 28 | units: "bytes", 29 | dataSourceKey: "runtime.MemStats.HeapAlloc", 30 | }, 31 | &baseGoMetrica{ 32 | name: "InUse/Stack", 33 | units: "bytes", 34 | dataSourceKey: "runtime.MemStats.StackInuse", 35 | }, 36 | &baseGoMetrica{ 37 | name: "InUse/MSpanInuse", 38 | units: "bytes", 39 | dataSourceKey: "runtime.MemStats.MSpanInuse", 40 | }, 41 | &baseGoMetrica{ 42 | name: "InUse/MCacheInuse", 43 | units: "bytes", 44 | dataSourceKey: "runtime.MemStats.MCacheInuse", 45 | }, 46 | } 47 | ds := newMemoryMetricaDataSource(pollInterval) 48 | for _, m := range gaugeMetrics { 49 | m.basePath = "Runtime/Memory/" 50 | m.dataSource = ds 51 | component.AddMetrica(&gaugeMetrica{m}) 52 | } 53 | 54 | gaugeIncMetrics := []*baseGoMetrica{ 55 | //NO operations graph 56 | &baseGoMetrica{ 57 | name: "Operations/NoPointerLookups", 58 | units: "lookups", 59 | dataSourceKey: "runtime.MemStats.Lookups", 60 | }, 61 | &baseGoMetrica{ 62 | name: "Operations/NoMallocs", 63 | units: "mallocs", 64 | dataSourceKey: "runtime.MemStats.Mallocs", 65 | }, 66 | &baseGoMetrica{ 67 | name: "Operations/NoFrees", 68 | units: "frees", 69 | dataSourceKey: "runtime.MemStats.Frees", 70 | }, 71 | 72 | // Sytem memory allocations 73 | &baseGoMetrica{ 74 | name: "SysMem/Total", 75 | units: "bytes", 76 | dataSourceKey: "runtime.MemStats.Sys", 77 | }, 78 | &baseGoMetrica{ 79 | name: "SysMem/Heap", 80 | units: "bytes", 81 | dataSourceKey: "runtime.MemStats.HeapSys", 82 | }, 83 | &baseGoMetrica{ 84 | name: "SysMem/Stack", 85 | units: "bytes", 86 | dataSourceKey: "runtime.MemStats.StackSys", 87 | }, 88 | &baseGoMetrica{ 89 | name: "SysMem/MSpan", 90 | units: "bytes", 91 | dataSourceKey: "runtime.MemStats.MSpanSys", 92 | }, 93 | &baseGoMetrica{ 94 | name: "SysMem/MCache", 95 | units: "bytes", 96 | dataSourceKey: "runtime.MemStats.MCacheSys", 97 | }, 98 | &baseGoMetrica{ 99 | name: "SysMem/BuckHash", 100 | units: "bytes", 101 | dataSourceKey: "runtime.MemStats.BuckHashSys", 102 | }, 103 | } 104 | 105 | for _, m := range gaugeIncMetrics { 106 | m.basePath = "Runtime/Memory/" 107 | m.dataSource = ds 108 | component.AddMetrica(&gaugeIncMetrica{baseGoMetrica: m}) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tracer_metrics.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | metrics "github.com/yvasiyarov/go-metrics" 8 | "github.com/yvasiyarov/newrelic_platform_go" 9 | ) 10 | 11 | type Tracer struct { 12 | sync.RWMutex 13 | metrics map[string]*TraceTransaction 14 | component newrelic_platform_go.IComponent 15 | } 16 | 17 | func newTracer(component newrelic_platform_go.IComponent) *Tracer { 18 | return &Tracer{metrics: make(map[string]*TraceTransaction), component: component} 19 | } 20 | 21 | func (t *Tracer) Trace(name string, traceFunc func()) { 22 | trace := t.BeginTrace(name) 23 | defer trace.EndTrace() 24 | traceFunc() 25 | } 26 | 27 | func (t *Tracer) BeginTrace(name string) *Trace { 28 | tracerName := "Trace/" + name 29 | 30 | // Happy path: The transaction has already been created, so just read it from the map. 31 | t.RLock() 32 | trans, ok := t.metrics[tracerName] 33 | t.RUnlock() 34 | if ok { 35 | return newTrace(trans) 36 | } 37 | 38 | // Slow path: We need to create the transaction and write it to the map, but first 39 | // we need to check if some other goroutine added the same transaction to the map in the 40 | // mean time. 41 | t.Lock() 42 | trans, ok = t.metrics[tracerName] 43 | if ok { 44 | t.Unlock() 45 | return newTrace(trans) 46 | } 47 | 48 | trans = &TraceTransaction{name: tracerName, timer: metrics.NewTimer()} 49 | 50 | t.metrics[tracerName] = trans 51 | t.Unlock() 52 | 53 | trans.addMetricsToComponent(t.component) 54 | 55 | return newTrace(trans) 56 | } 57 | 58 | type Trace struct { 59 | transaction *TraceTransaction 60 | startTime time.Time 61 | } 62 | 63 | func (t *Trace) EndTrace() { 64 | t.transaction.timer.UpdateSince(t.startTime) 65 | } 66 | 67 | func newTrace(trans *TraceTransaction) *Trace { 68 | return &Trace{transaction: trans, startTime: time.Now()} 69 | } 70 | 71 | type TraceTransaction struct { 72 | name string 73 | timer metrics.Timer 74 | } 75 | 76 | func (transaction *TraceTransaction) addMetricsToComponent(component newrelic_platform_go.IComponent) { 77 | tracerMean := &timerMeanMetrica{ 78 | baseTimerMetrica: &baseTimerMetrica{ 79 | name: transaction.name + "/mean", 80 | units: "ms", 81 | dataSource: transaction.timer, 82 | }, 83 | } 84 | component.AddMetrica(tracerMean) 85 | 86 | tracerMax := &timerMaxMetrica{ 87 | baseTimerMetrica: &baseTimerMetrica{ 88 | name: transaction.name + "/max", 89 | units: "ms", 90 | dataSource: transaction.timer, 91 | }, 92 | } 93 | component.AddMetrica(tracerMax) 94 | 95 | tracerMin := &timerMinMetrica{ 96 | baseTimerMetrica: &baseTimerMetrica{ 97 | name: transaction.name + "/min", 98 | units: "ms", 99 | dataSource: transaction.timer, 100 | }, 101 | } 102 | component.AddMetrica(tracerMin) 103 | 104 | tracer75 := &timerPercentile75Metrica{ 105 | baseTimerMetrica: &baseTimerMetrica{ 106 | name: transaction.name + "/percentile75", 107 | units: "ms", 108 | dataSource: transaction.timer, 109 | }, 110 | } 111 | component.AddMetrica(tracer75) 112 | 113 | tracer90 := &timerPercentile90Metrica{ 114 | baseTimerMetrica: &baseTimerMetrica{ 115 | name: transaction.name + "/percentile90", 116 | units: "ms", 117 | dataSource: transaction.timer, 118 | }, 119 | } 120 | component.AddMetrica(tracer90) 121 | 122 | tracer95 := &timerPercentile95Metrica{ 123 | baseTimerMetrica: &baseTimerMetrica{ 124 | name: transaction.name + "/percentile95", 125 | units: "ms", 126 | dataSource: transaction.timer, 127 | }, 128 | } 129 | component.AddMetrica(tracer95) 130 | } 131 | -------------------------------------------------------------------------------- /http_metrics.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | metrics "github.com/yvasiyarov/go-metrics" 5 | "github.com/yvasiyarov/newrelic_platform_go" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type tHTTPHandlerFunc func(http.ResponseWriter, *http.Request) 11 | type tHTTPHandler struct { 12 | originalHandler http.Handler 13 | originalHandlerFunc tHTTPHandlerFunc 14 | isFunc bool 15 | timer metrics.Timer 16 | httpStatusCounters map[int]metrics.Counter 17 | } 18 | 19 | //A wrapper over a http.ResponseWriter object 20 | type responseWriterWrapper struct { 21 | originalWriter http.ResponseWriter 22 | statusCode int 23 | } 24 | 25 | var httpTimer metrics.Timer 26 | 27 | func (reponseWriterWrapper *responseWriterWrapper) Header() http.Header { 28 | return reponseWriterWrapper.originalWriter.Header() 29 | } 30 | 31 | func (reponseWriterWrapper *responseWriterWrapper) Write(bytes []byte) (int, error) { 32 | return reponseWriterWrapper.originalWriter.Write(bytes) 33 | } 34 | 35 | func (reponseWriterWrapper *responseWriterWrapper) WriteHeader(code int) { 36 | reponseWriterWrapper.originalWriter.WriteHeader(code) 37 | reponseWriterWrapper.statusCode = code 38 | } 39 | 40 | func newHTTPHandlerFunc(h tHTTPHandlerFunc) *tHTTPHandler { 41 | return &tHTTPHandler{ 42 | isFunc: true, 43 | originalHandlerFunc: h, 44 | } 45 | } 46 | func newHTTPHandler(h http.Handler) *tHTTPHandler { 47 | return &tHTTPHandler{ 48 | isFunc: false, 49 | originalHandler: h, 50 | } 51 | } 52 | 53 | func (handler *tHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 54 | startTime := time.Now() 55 | defer handler.timer.UpdateSince(startTime) 56 | 57 | responseWriterWrapper := responseWriterWrapper{originalWriter: w, statusCode: http.StatusOK} 58 | 59 | if handler.isFunc { 60 | handler.originalHandlerFunc(&responseWriterWrapper, req) 61 | } else { 62 | handler.originalHandler.ServeHTTP(&responseWriterWrapper, req) 63 | } 64 | 65 | if statusCounter := handler.httpStatusCounters[responseWriterWrapper.statusCode]; statusCounter != nil { 66 | statusCounter.Inc(1) 67 | } 68 | } 69 | 70 | func addHTTPMericsToComponent(component newrelic_platform_go.IComponent, timer metrics.Timer) { 71 | rate1 := &timerRate1Metrica{ 72 | baseTimerMetrica: &baseTimerMetrica{ 73 | name: "http/throughput/1minute", 74 | units: "rps", 75 | dataSource: timer, 76 | }, 77 | } 78 | component.AddMetrica(rate1) 79 | 80 | rateMean := &timerRateMeanMetrica{ 81 | baseTimerMetrica: &baseTimerMetrica{ 82 | name: "http/throughput/rateMean", 83 | units: "rps", 84 | dataSource: timer, 85 | }, 86 | } 87 | component.AddMetrica(rateMean) 88 | 89 | responseTimeMean := &timerMeanMetrica{ 90 | baseTimerMetrica: &baseTimerMetrica{ 91 | name: "http/responseTime/mean", 92 | units: "ms", 93 | dataSource: timer, 94 | }, 95 | } 96 | component.AddMetrica(responseTimeMean) 97 | 98 | responseTimeMax := &timerMaxMetrica{ 99 | baseTimerMetrica: &baseTimerMetrica{ 100 | name: "http/responseTime/max", 101 | units: "ms", 102 | dataSource: timer, 103 | }, 104 | } 105 | component.AddMetrica(responseTimeMax) 106 | 107 | responseTimeMin := &timerMinMetrica{ 108 | baseTimerMetrica: &baseTimerMetrica{ 109 | name: "http/responseTime/min", 110 | units: "ms", 111 | dataSource: timer, 112 | }, 113 | } 114 | component.AddMetrica(responseTimeMin) 115 | 116 | responseTimePercentile75 := &timerPercentile75Metrica{ 117 | baseTimerMetrica: &baseTimerMetrica{ 118 | name: "http/responseTime/percentile75", 119 | units: "ms", 120 | dataSource: timer, 121 | }, 122 | } 123 | component.AddMetrica(responseTimePercentile75) 124 | 125 | responseTimePercentile90 := &timerPercentile90Metrica{ 126 | baseTimerMetrica: &baseTimerMetrica{ 127 | name: "http/responseTime/percentile90", 128 | units: "ms", 129 | dataSource: timer, 130 | }, 131 | } 132 | component.AddMetrica(responseTimePercentile90) 133 | 134 | responseTimePercentile95 := &timerPercentile95Metrica{ 135 | baseTimerMetrica: &baseTimerMetrica{ 136 | name: "http/responseTime/percentile95", 137 | units: "ms", 138 | dataSource: timer, 139 | }, 140 | } 141 | component.AddMetrica(responseTimePercentile95) 142 | } 143 | -------------------------------------------------------------------------------- /runtime_metrics.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | "fmt" 5 | "github.com/yvasiyarov/newrelic_platform_go" 6 | "io/ioutil" 7 | "os" 8 | "runtime" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | const linuxSystemQueryInterval = 60 15 | 16 | // Number of goroutines metrica 17 | type noGoroutinesMetrica struct{} 18 | 19 | func (metrica *noGoroutinesMetrica) GetName() string { 20 | return "Runtime/General/NOGoroutines" 21 | } 22 | func (metrica *noGoroutinesMetrica) GetUnits() string { 23 | return "goroutines" 24 | } 25 | func (metrica *noGoroutinesMetrica) GetValue() (float64, error) { 26 | return float64(runtime.NumGoroutine()), nil 27 | } 28 | 29 | // Number of CGO calls metrica 30 | type noCgoCallsMetrica struct { 31 | lastValue int64 32 | } 33 | 34 | func (metrica *noCgoCallsMetrica) GetName() string { 35 | return "Runtime/General/NOCgoCalls" 36 | } 37 | func (metrica *noCgoCallsMetrica) GetUnits() string { 38 | return "calls" 39 | } 40 | func (metrica *noCgoCallsMetrica) GetValue() (float64, error) { 41 | currentValue := runtime.NumCgoCall() 42 | value := float64(currentValue - metrica.lastValue) 43 | metrica.lastValue = currentValue 44 | 45 | return value, nil 46 | } 47 | 48 | //OS specific metrics data source interface 49 | type iSystemMetricaDataSource interface { 50 | GetValue(key string) (float64, error) 51 | } 52 | 53 | // iSystemMetricaDataSource fabrica 54 | func newSystemMetricaDataSource() iSystemMetricaDataSource { 55 | var ds iSystemMetricaDataSource 56 | switch runtime.GOOS { 57 | default: 58 | ds = &systemMetricaDataSource{} 59 | case "linux": 60 | ds = &linuxSystemMetricaDataSource{ 61 | systemData: make(map[string]string), 62 | } 63 | } 64 | return ds 65 | } 66 | 67 | //Default implementation of iSystemMetricaDataSource. Just return an error 68 | type systemMetricaDataSource struct{} 69 | 70 | func (ds *systemMetricaDataSource) GetValue(key string) (float64, error) { 71 | return 0, fmt.Errorf("this metrica was not implemented yet for %s", runtime.GOOS) 72 | } 73 | 74 | // Linux OS implementation of ISystemMetricaDataSource 75 | type linuxSystemMetricaDataSource struct { 76 | lastUpdate time.Time 77 | systemData map[string]string 78 | } 79 | 80 | func (ds *linuxSystemMetricaDataSource) GetValue(key string) (float64, error) { 81 | if err := ds.checkAndUpdateData(); err != nil { 82 | return 0, err 83 | } else if val, ok := ds.systemData[key]; !ok { 84 | return 0, fmt.Errorf("system data with key %s was not found", key) 85 | } else if key == "VmSize" || key == "VmPeak" || key == "VmHWM" || key == "VmRSS" { 86 | valueParts := strings.Split(val, " ") 87 | if len(valueParts) != 2 { 88 | return 0, fmt.Errorf("invalid format for value %s", key) 89 | } 90 | valConverted, err := strconv.ParseFloat(valueParts[0], 64) 91 | if err != nil { 92 | return 0, err 93 | } 94 | switch valueParts[1] { 95 | case "kB": 96 | valConverted *= 1 << 10 97 | case "mB": 98 | valConverted *= 1 << 20 99 | case "gB": 100 | valConverted *= 1 << 30 101 | } 102 | return valConverted, nil 103 | } else if valConverted, err := strconv.ParseFloat(val, 64); err != nil { 104 | return valConverted, nil 105 | } else { 106 | return valConverted, nil 107 | } 108 | } 109 | func (ds *linuxSystemMetricaDataSource) checkAndUpdateData() error { 110 | startTime := time.Now() 111 | if startTime.Sub(ds.lastUpdate) > time.Second*linuxSystemQueryInterval { 112 | path := fmt.Sprintf("/proc/%d/status", os.Getpid()) 113 | rawStats, err := ioutil.ReadFile(path) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | lines := strings.Split(string(rawStats), "\n") 119 | for _, line := range lines { 120 | parts := strings.Split(line, ":") 121 | if len(parts) == 2 { 122 | k := strings.TrimSpace(parts[0]) 123 | v := strings.TrimSpace(parts[1]) 124 | 125 | ds.systemData[k] = v 126 | } 127 | } 128 | ds.lastUpdate = startTime 129 | } 130 | return nil 131 | } 132 | 133 | // OS specific metrica 134 | type systemMetrica struct { 135 | sourceKey string 136 | newrelicName string 137 | units string 138 | dataSource iSystemMetricaDataSource 139 | } 140 | 141 | func (metrica *systemMetrica) GetName() string { 142 | return metrica.newrelicName 143 | } 144 | func (metrica *systemMetrica) GetUnits() string { 145 | return metrica.units 146 | } 147 | func (metrica *systemMetrica) GetValue() (float64, error) { 148 | return metrica.dataSource.GetValue(metrica.sourceKey) 149 | } 150 | 151 | func addRuntimeMericsToComponent(component newrelic_platform_go.IComponent) { 152 | component.AddMetrica(&noGoroutinesMetrica{}) 153 | component.AddMetrica(&noCgoCallsMetrica{}) 154 | 155 | ds := newSystemMetricaDataSource() 156 | metrics := []*systemMetrica{ 157 | &systemMetrica{ 158 | sourceKey: "Threads", 159 | units: "Threads", 160 | newrelicName: "Runtime/System/Threads", 161 | }, 162 | &systemMetrica{ 163 | sourceKey: "FDSize", 164 | units: "fd", 165 | newrelicName: "Runtime/System/FDSize", 166 | }, 167 | // Peak virtual memory size 168 | &systemMetrica{ 169 | sourceKey: "VmPeak", 170 | units: "bytes", 171 | newrelicName: "Runtime/System/Memory/VmPeakSize", 172 | }, 173 | //Virtual memory size 174 | &systemMetrica{ 175 | sourceKey: "VmSize", 176 | units: "bytes", 177 | newrelicName: "Runtime/System/Memory/VmCurrent", 178 | }, 179 | //Peak resident set size 180 | &systemMetrica{ 181 | sourceKey: "VmHWM", 182 | units: "bytes", 183 | newrelicName: "Runtime/System/Memory/RssPeak", 184 | }, 185 | //Resident set size 186 | &systemMetrica{ 187 | sourceKey: "VmRSS", 188 | units: "bytes", 189 | newrelicName: "Runtime/System/Memory/RssCurrent", 190 | }, 191 | } 192 | for _, m := range metrics { 193 | m.dataSource = ds 194 | component.AddMetrica(m) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoRelic is deprecated in favour of [https://github.com/newrelic/go-agent](https://github.com/newrelic/go-agent) 2 | 3 | New Relic agent for Go runtime. It collect a lot of metrics about scheduler, garbage collector and memory allocator and 4 | send them to NewRelic. 5 | 6 | ### Requirements 7 | - Go 1.1 or higher 8 | - github.com/yvasiyarov/gorelic 9 | - github.com/yvasiyarov/newrelic_platform_go 10 | - github.com/yvasiyarov/go-metrics 11 | 12 | You have to install manually only first two dependencies. All other dependencies will be installed automatically 13 | by Go toolchain. 14 | 15 | ### Installation 16 | ```bash 17 | go get github.com/yvasiyarov/gorelic 18 | ``` 19 | and add to the initialization part of your application following code: 20 | ```go 21 | import ( 22 | "github.com/yvasiyarov/gorelic" 23 | ) 24 | .... 25 | 26 | agent := gorelic.NewAgent() 27 | agent.Verbose = true 28 | agent.NewrelicLicense = "YOUR NEWRELIC LICENSE KEY THERE" 29 | agent.Run() 30 | 31 | ``` 32 | 33 | ### Middleware 34 | If you using Beego, Martini, Revel, Kami or Gin framework you can hook up gorelic with your application by using the following middleware: 35 | - https://github.com/yvasiyarov/beego_gorelic 36 | - https://github.com/yvasiyarov/martini_gorelic 37 | - https://github.com/yvasiyarov/gocraft_gorelic 38 | - http://wiki.colar.net/revel_newelic 39 | - https://github.com/jingweno/negroni-gorelic 40 | - https://github.com/brandfolder/gin-gorelic 41 | - [https://github.com/syntaqx/echo-middleware/gorelic](https://github.com/syntaqx/echo-middleware/tree/master/gorelic) 42 | - https://github.com/david4shure/kamigorelic 43 | 44 | ### Configuration 45 | - NewrelicLicense - its the only mandatory setting of this agent. 46 | - NewrelicName - component name in NewRelic dashboard. Default value: "Go daemon" 47 | - NewrelicPollInterval - how often metrics will be sent to NewRelic. Default value: 60 seconds 48 | - Verbose - print some usefull for debugging information. Default value: false 49 | - CollectGcStat - should agent collect garbage collector statistic or not. Default value: true 50 | - CollectHTTPStat - should agent collect HTTP metrics. Default value: false 51 | - CollectHTTPStatuses - should agent collect metrics on HTTP status codes. Default value: false 52 | - CollectMemoryStat - should agent collect memory allocator statistic or not. Default value: true 53 | - GCPollInterval - how often should GC statistic collected. Default value: 10 seconds. It has performance impact. For more information, please, see metrics documentation. 54 | - MemoryAllocatorPollInterval - how often should memory allocator statistic collected. Default value: 60 seconds. It has performance impact. For more information, please, read metrics documentation. 55 | 56 | 57 | ## Metrics reported by plugin 58 | This agent use functions exposed by runtime or runtime/debug packages to collect most important information about Go runtime. 59 | 60 | ### General metrics 61 | - Runtime/General/NOGoroutines - number of runned go routines, as it reported by NumGoroutine() from runtime package 62 | - Runtime/General/NOCgoCalls - number of runned cgo calls, as it reported by NumCgoCall() from runtime package 63 | 64 | ### Garbage collector metrics 65 | - Runtime/GC/NumberOfGCCalls - Nuber of GC calls, as it reported by ReadGCStats() from runtime/debug 66 | - Runtime/GC/PauseTotalTime - Total pause time diring GC calls, as it reported by ReadGCStats() from runtime/debug (in nanoseconds) 67 | - Runtime/GC/GCTime/Max - max GC time 68 | - Runtime/GC/GCTime/Min - min GC time 69 | - Runtime/GC/GCTime/Mean - GC mean time 70 | - Runtime/GC/GCTime/Percentile95 - 95% percentile of GC time 71 | 72 | All this metrics are measured in nanoseconds. Last 4 of them can be inaccurate if GC called more often then once in GCPollInterval. 73 | If in your workload GC is called more often - you can consider decreasing value of GCPollInterval. 74 | But be careful, ReadGCStats() blocks mheap, so its not good idea to set GCPollInterval to very low values. 75 | 76 | ### Memory allocator 77 | - Component/Runtime/Memory/SysMem/Total - number of bytes/minute allocated from OS totally. 78 | - Component/Runtime/Memory/SysMem/Stack - number of bytes/minute allocated from OS for stacks. 79 | - Component/Runtime/Memory/SysMem/MSpan - number of bytes/minute allocated from OS for internal MSpan structs. 80 | - Component/Runtime/Memory/SysMem/MCache - number of bytes/minute allocated from OS for internal MCache structs. 81 | - Component/Runtime/Memory/SysMem/Heap - number of bytes/minute allocated from OS for heap. 82 | - Component/Runtime/Memory/SysMem/BuckHash - number of bytes/minute allocated from OS for internal BuckHash structs. 83 | - Component/Runtime/Memory/Operations/NoFrees - number of memory frees per minute 84 | - Component/Runtime/Memory/Operations/NoMallocs - number of memory allocations per minute 85 | - Component/Runtime/Memory/Operations/NoPointerLookups - number of pointer lookups per minute 86 | - Component/Runtime/Memory/InUse/Total - total amount of memory in use 87 | - Component/Runtime/Memory/InUse/Heap - amount of memory in use for heap 88 | - Component/Runtime/Memory/InUse/MCacheInuse - amount of memory in use for MCache internal structures 89 | - Component/Runtime/Memory/InUse/MSpanInuse - amount of memory in use for MSpan internal structures 90 | - Component/Runtime/Memory/InUse/Stack - amount of memory in use for stacks 91 | 92 | ### Process metrics 93 | - Component/Runtime/System/Threads - number of OS threads used 94 | - Runtime/System/FDSize - number of file descriptors, used by process 95 | - Runtime/System/Memory/VmPeakSize - VM max size 96 | - Runtime/System/Memory/VmCurrent - VM current size 97 | - Runtime/System/Memory/RssPeak - max size of resident memory set 98 | - Runtime/System/Memory/RssCurrent - current size of resident memory set 99 | 100 | All this metrics collected once in MemoryAllocatorPollInterval. In order to collect this statistic agent use ReadMemStats() routine. 101 | This routine calls stoptheworld() internally and it block everything. So, please, consider this when you change MemoryAllocatorPollInterval value. 102 | 103 | ### HTTP metrics 104 | - throughput (requests per second), calculated for last minute 105 | - mean throughput (requests per second) 106 | - mean response time 107 | - min response time 108 | - max response time 109 | - 75%, 90%, 95% percentiles for response time 110 | 111 | 112 | In order to collect HTTP metrics, handler functions must be wrapped using WrapHTTPHandlerFunc: 113 | 114 | ```go 115 | http.HandleFunc("/", agent.WrapHTTPHandlerFunc(handler)) 116 | ``` 117 | ### Tracing Metrics 118 | You can collect metrics for blocks of code or methods. 119 | ```go 120 | func anyMethod() { 121 | // Trace the whole method. 122 | t := agent.Tracer.BeginTrace("My traced method") 123 | defer t.EndTrace() 124 | 125 | ...Code here 126 | 127 | // Trace a block of code 128 | agent.Tracer.Trace("block trace", func() { 129 | .. Code here 130 | }) 131 | } 132 | ``` 133 | ## TODO 134 | - Collect per-size allocation statistic 135 | - Collect user defined metrics 136 | 137 | -------------------------------------------------------------------------------- /agent.go: -------------------------------------------------------------------------------- 1 | package gorelic 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | metrics "github.com/yvasiyarov/go-metrics" 10 | "github.com/yvasiyarov/newrelic_platform_go" 11 | ) 12 | 13 | const ( 14 | // DefaultNewRelicPollInterval - how often we will report metrics to NewRelic. 15 | // Recommended values is 60 seconds 16 | DefaultNewRelicPollInterval = 60 17 | 18 | // DefaultGcPollIntervalInSeconds - how often we will get garbage collector run statistic 19 | // Default value is - every 10 seconds 20 | // During GC stat pooling - mheap will be locked, so be carefull changing this value 21 | DefaultGcPollIntervalInSeconds = 10 22 | 23 | // DefaultMemoryAllocatorPollIntervalInSeconds - how often we will get memory allocator statistic. 24 | // Default value is - every 60 seconds 25 | // During this process stoptheword() is called, so be carefull changing this value 26 | DefaultMemoryAllocatorPollIntervalInSeconds = 60 27 | 28 | //DefaultAgentGuid is plugin ID in NewRelic. 29 | //You should not change it unless you want to create your own plugin. 30 | DefaultAgentGuid = "com.github.yvasiyarov.GoRelic" 31 | 32 | //CurrentAgentVersion is plugin version 33 | CurrentAgentVersion = "0.0.6" 34 | 35 | //DefaultAgentName in NewRelic GUI. You can change it. 36 | DefaultAgentName = "Go daemon" 37 | ) 38 | 39 | //Agent - is NewRelic agent implementation. 40 | //Agent start separate go routine which will report data to NewRelic 41 | type Agent struct { 42 | NewrelicName string 43 | NewrelicLicense string 44 | NewrelicPollInterval int 45 | Verbose bool 46 | CollectGcStat bool 47 | CollectMemoryStat bool 48 | CollectHTTPStat bool 49 | CollectHTTPStatuses bool 50 | GCPollInterval int 51 | MemoryAllocatorPollInterval int 52 | AgentGUID string 53 | AgentVersion string 54 | plugin *newrelic_platform_go.NewrelicPlugin 55 | HTTPTimer metrics.Timer 56 | HTTPStatusCounters map[int]metrics.Counter 57 | Tracer *Tracer 58 | CustomMetrics []newrelic_platform_go.IMetrica 59 | 60 | // All HTTP requests will be done using this client. Change it if you need 61 | // to use a proxy. 62 | Client http.Client 63 | } 64 | 65 | // NewAgent builds new Agent objects. 66 | func NewAgent() *Agent { 67 | agent := &Agent{ 68 | NewrelicName: DefaultAgentName, 69 | NewrelicPollInterval: DefaultNewRelicPollInterval, 70 | Verbose: false, 71 | CollectGcStat: true, 72 | CollectMemoryStat: true, 73 | GCPollInterval: DefaultGcPollIntervalInSeconds, 74 | MemoryAllocatorPollInterval: DefaultMemoryAllocatorPollIntervalInSeconds, 75 | AgentGUID: DefaultAgentGuid, 76 | AgentVersion: CurrentAgentVersion, 77 | Tracer: nil, 78 | CustomMetrics: make([]newrelic_platform_go.IMetrica, 0), 79 | } 80 | return agent 81 | } 82 | 83 | // our custom component 84 | type resettableComponent struct { 85 | newrelic_platform_go.IComponent 86 | counters map[int]metrics.Counter 87 | } 88 | 89 | // ClearSentData: newrelic_platform_go.IComponent interface implementation 90 | func (c resettableComponent) ClearSentData() { 91 | c.IComponent.ClearSentData() 92 | for _, counter := range c.counters { 93 | counter.Clear() 94 | } 95 | } 96 | 97 | //WrapHTTPHandlerFunc instrument HTTP handler functions to collect HTTP metrics 98 | func (agent *Agent) WrapHTTPHandlerFunc(h tHTTPHandlerFunc) tHTTPHandlerFunc { 99 | agent.CollectHTTPStat = true 100 | agent.initTimer() 101 | return func(w http.ResponseWriter, req *http.Request) { 102 | proxy := newHTTPHandlerFunc(h) 103 | proxy.timer = agent.HTTPTimer 104 | //set the http status counters before serving request. 105 | proxy.httpStatusCounters = agent.HTTPStatusCounters 106 | proxy.ServeHTTP(w, req) 107 | } 108 | } 109 | 110 | //WrapHTTPHandler instrument HTTP handler object to collect HTTP metrics 111 | func (agent *Agent) WrapHTTPHandler(h http.Handler) http.Handler { 112 | agent.CollectHTTPStat = true 113 | agent.initTimer() 114 | 115 | proxy := newHTTPHandler(h) 116 | proxy.timer = agent.HTTPTimer 117 | return proxy 118 | } 119 | 120 | //AddCustomMetric adds metric to be collected periodically with NewrelicPollInterval interval 121 | func (agent *Agent) AddCustomMetric(metric newrelic_platform_go.IMetrica) { 122 | agent.CustomMetrics = append(agent.CustomMetrics, metric) 123 | } 124 | 125 | //Run initialize Agent instance and start harvest go routine 126 | func (agent *Agent) Run() error { 127 | if agent.NewrelicLicense == "" { 128 | return errors.New("please, pass a valid newrelic license key") 129 | } 130 | 131 | var component newrelic_platform_go.IComponent 132 | component = newrelic_platform_go.NewPluginComponent(agent.NewrelicName, agent.AgentGUID, agent.Verbose) 133 | 134 | // Add default metrics and tracer. 135 | addRuntimeMericsToComponent(component) 136 | agent.Tracer = newTracer(component) 137 | 138 | // Check agent flags and add relevant metrics. 139 | if agent.CollectGcStat { 140 | addGCMetricsToComponent(component, agent.GCPollInterval) 141 | agent.debug(fmt.Sprintf("Init GC metrics collection. Poll interval %d seconds.", agent.GCPollInterval)) 142 | } 143 | 144 | if agent.CollectMemoryStat { 145 | addMemoryMericsToComponent(component, agent.MemoryAllocatorPollInterval) 146 | agent.debug(fmt.Sprintf("Init memory allocator metrics collection. Poll interval %d seconds.", agent.MemoryAllocatorPollInterval)) 147 | } 148 | 149 | if agent.CollectHTTPStat { 150 | agent.initTimer() 151 | addHTTPMericsToComponent(component, agent.HTTPTimer) 152 | agent.debug(fmt.Sprintf("Init HTTP metrics collection.")) 153 | } 154 | 155 | for _, metric := range agent.CustomMetrics { 156 | component.AddMetrica(metric) 157 | agent.debug(fmt.Sprintf("Init %s metric collection.", metric.GetName())) 158 | } 159 | 160 | if agent.CollectHTTPStatuses { 161 | agent.initStatusCounters() 162 | component = &resettableComponent{component, agent.HTTPStatusCounters} 163 | addHTTPStatusMetricsToComponent(component, agent.HTTPStatusCounters) 164 | agent.debug(fmt.Sprintf("Init HTTP status metrics collection.")) 165 | } 166 | 167 | // Init newrelic reporting plugin. 168 | agent.plugin = newrelic_platform_go.NewNewrelicPlugin(agent.AgentVersion, agent.NewrelicLicense, agent.NewrelicPollInterval) 169 | agent.plugin.Client = agent.Client 170 | agent.plugin.Verbose = agent.Verbose 171 | 172 | // Add our metrics component to the plugin. 173 | agent.plugin.AddComponent(component) 174 | 175 | // Start reporting! 176 | go agent.plugin.Run() 177 | return nil 178 | } 179 | 180 | //Initialize global metrics.Timer object, used to collect HTTP metrics 181 | func (agent *Agent) initTimer() { 182 | if agent.HTTPTimer == nil { 183 | agent.HTTPTimer = metrics.NewTimer() 184 | } 185 | } 186 | 187 | //Initialize metrics.Counters objects, used to collect HTTP statuses 188 | func (agent *Agent) initStatusCounters() { 189 | httpStatuses := []int{ 190 | http.StatusContinue, http.StatusSwitchingProtocols, 191 | 192 | http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNonAuthoritativeInfo, 193 | http.StatusNoContent, http.StatusResetContent, http.StatusPartialContent, 194 | 195 | http.StatusMultipleChoices, http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, 196 | http.StatusNotModified, http.StatusUseProxy, http.StatusTemporaryRedirect, 197 | 198 | http.StatusBadRequest, http.StatusUnauthorized, http.StatusPaymentRequired, http.StatusForbidden, 199 | http.StatusNotFound, http.StatusMethodNotAllowed, http.StatusNotAcceptable, http.StatusProxyAuthRequired, 200 | http.StatusRequestTimeout, http.StatusConflict, http.StatusGone, http.StatusLengthRequired, 201 | http.StatusPreconditionFailed, http.StatusRequestEntityTooLarge, http.StatusRequestURITooLong, http.StatusUnsupportedMediaType, 202 | http.StatusRequestedRangeNotSatisfiable, http.StatusExpectationFailed, http.StatusTeapot, 203 | 204 | http.StatusInternalServerError, http.StatusNotImplemented, http.StatusBadGateway, 205 | http.StatusServiceUnavailable, http.StatusGatewayTimeout, http.StatusHTTPVersionNotSupported, 206 | } 207 | 208 | agent.HTTPStatusCounters = make(map[int]metrics.Counter, len(httpStatuses)) 209 | for _, statusCode := range httpStatuses { 210 | agent.HTTPStatusCounters[statusCode] = metrics.NewCounter() 211 | } 212 | } 213 | 214 | //Print debug messages 215 | func (agent *Agent) debug(msg string) { 216 | if agent.Verbose { 217 | log.Println(msg) 218 | } 219 | } 220 | --------------------------------------------------------------------------------