├── .drone.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets └── gopher-pm2.png ├── features ├── metrics │ ├── internal.go │ ├── internal_test.go │ ├── system.go │ └── system_test.go ├── notifier.go ├── profiling_test.go ├── profling.go ├── tracing.go └── tracing_model.go ├── go.mod ├── go.sum ├── pm2io.go ├── services ├── action.go ├── action_test.go ├── metric.go ├── metric_test.go ├── structure.go ├── transport.go └── transport_test.go ├── src └── main.go └── structures ├── action.go ├── config.go ├── config_test.go ├── metric.go ├── metric_test.go ├── options.go ├── process.go ├── profiling.go ├── server.go ├── status.go └── statusprocess.go /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | name: Build and tests 4 | 5 | workspace: 6 | base: /go 7 | path: src/github.com/keymetrics/pm2-io-apm-go 8 | 9 | steps: 10 | - name: tests 11 | image: golang:latest 12 | commands: 13 | - go get -v 14 | - go get gopkg.in/h2non/gock.v1 15 | - "curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter" 16 | - chmod +x ./cc-test-reporter 17 | - ./cc-test-reporter before-build 18 | - go test -v -coverprofile=c.out -covermode=atomic $(go list ./...) 19 | - "./cc-test-reporter after-build --exit-code 0 || echo “Skipping CC coverage upload” or upload-coverage || echo “Skipping CC coverage upload”" 20 | - go build 21 | environment: 22 | CC_TEST_REPORTER_ID: 23 | from_secret: coverage_token 24 | 25 | --- 26 | kind: secret 27 | name: coverage_token 28 | get: 29 | path: secret/drone/codeclimate 30 | name: token -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | main 15 | coverage.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Keymetrics, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](assets/gopher-pm2.png) 2 | 3 | # PM2/io APM Golang 4 | This PM2 module is standalone and must include a public/private key to working properly with PM2 Plus. 5 | 6 | ## Init 7 | 8 | ```golang 9 | package main 10 | 11 | import ( 12 | "github.com/keymetrics/pm2-io-apm-go/services" 13 | "github.com/keymetrics/pm2-io-apm-go/structures" 14 | ) 15 | 16 | func main() { 17 | // Create PM2 connector 18 | pm2 := pm2io.Pm2Io{ 19 | Config: &structures.Config{ 20 | PublicKey: "myPublic", 21 | PrivateKey: "myPrivate", 22 | Name: "Golang app", 23 | }, 24 | } 25 | 26 | // Add an action who can be triggered from PM2 Plus 27 | services.AddAction(&structures.Action{ 28 | ActionName: "Get env", 29 | Callback: func(_ map[string]interface{}) string { 30 | return strings.Join(os.Environ(), "\n") 31 | }, 32 | }) 33 | 34 | // Add a function metric who will be aggregated 35 | nbd := structures.CreateFuncMetric("Function metric", "metric", "stable/integer", func() float64 { 36 | // For a FuncMetric, this will be called every ~1sec 37 | return float64(10) 38 | }) 39 | services.AddMetric(&nbd) 40 | 41 | // Add a normal metric 42 | nbreq := structures.CreateMetric("Incrementable", "metric", "increments") 43 | services.AddMetric(&nbreq) 44 | 45 | // Goroutine who increment the value each 4 seconds 46 | go func() { 47 | ticker := time.NewTicker(4 * time.Second) 48 | for { 49 | <-ticker.C 50 | nbreq.Value++ 51 | 52 | // Log to PM2 Plus 53 | pm2io.Notifier.Log("Value incremented") 54 | } 55 | }() 56 | 57 | // Start the connection to PM2 Plus servers 58 | pm2.Start() 59 | 60 | // Log that we started the program (optional, just for example) 61 | pm2io.Notifier.Log("Started") 62 | 63 | // Wait infinitely (for example) 64 | <-time.After(time.Duration(math.MaxInt64)) 65 | } 66 | ``` 67 | 68 | ## Send error then panic 69 | You can send an error to PM2 Plus before panic your program 70 | ```golang 71 | name, err := os.Hostname() 72 | if err != nil { 73 | pm2io.Panic(err) 74 | // The program will crash with panic just after the message is sent 75 | } 76 | ``` 77 | 78 | ## Use a proxy for APM requests and WebSocket 79 | ```golang 80 | pm2io.Pm2Io{ 81 | Config: &structures.Config{ 82 | PublicKey: "myPublic", 83 | PrivateKey: "myPrivate", 84 | Name: "Golang app", 85 | Proxy: "socks5://localhost:1080/", 86 | }, 87 | } 88 | ``` 89 | 90 | ## Connect logrus to PM2 Plus 91 | If you are using logrus, this is an example to send logs and create exceptions on PM2 Plus when you log an error 92 | 93 | ```golang 94 | package main 95 | 96 | import ( 97 | pm2io "github.com/keymetrics/pm2-io-apm-go" 98 | "github.com/sirupsen/logrus" 99 | ) 100 | 101 | // HookLog will send logs to PM2 Plus 102 | type HookLog struct { 103 | Pm2 *pm2io.Pm2Io 104 | } 105 | 106 | // HookErr will send all errors to PM2 Plus 107 | type HookErr struct { 108 | Pm2 *pm2io.Pm2Io 109 | } 110 | 111 | // Fire event 112 | func (hook *HookLog) Fire(e *logrus.Entry) error { 113 | str, err := e.String() 114 | if err == nil { 115 | hook.Pm2.Notifier.Log(str) 116 | } 117 | return err 118 | } 119 | 120 | // Levels for all possible logs 121 | func (*HookLog) Levels() []logrus.Level { 122 | return logrus.AllLevels 123 | } 124 | 125 | // Fire an error and notify it as exception 126 | func (hook *HookErr) Fire(e *logrus.Entry) error { 127 | if err, ok := e.Data["error"].(error); ok { 128 | hook.Pm2.Notifier.Error(err) 129 | } 130 | return nil 131 | } 132 | 133 | // Levels only for errors 134 | func (*HookErr) Levels() []logrus.Level { 135 | return []logrus.Level{logrus.ErrorLevel} 136 | } 137 | 138 | func main() { 139 | pm2 := pm2io.Pm2Io{ 140 | Config: &structures.Config{ 141 | PublicKey: "myPublic", 142 | PrivateKey: "myPrivate", 143 | Name: "Golang app", 144 | }, 145 | } 146 | 147 | logrus.AddHook(&HookLog{ 148 | Pm2: pm2, 149 | }) 150 | logrus.AddHook(&HookErr{ 151 | Pm2: pm2, 152 | }) 153 | } 154 | ``` 155 | 156 | ## Distributed Tracing 157 | NB: you must pass the same `context.Context` to your subfuncs (same traceID). 158 | It's very useful when you want the same trace for HTTP spans and your database spans 159 | 160 | [More informations on OpenCensus](https://opencensus.io/guides/) 161 | 162 | ### HTTP 163 | OpenCensus will create another handler with wrapped functions. It's important to use contexts everytime 164 | 165 | For client and server, it use B3 for communication by default 166 | 167 | [OpenCensus Documentation about ochttp](https://opencensus.io/guides/http/go/net_http/) 168 | ```golang 169 | import ( 170 | "net/http" 171 | 172 | "go.opencensus.io/plugin/ochttp" 173 | ) 174 | 175 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 176 | context := r.Context() 177 | // Not used here, but you must pass it to lower functions (espacially databases one) 178 | 179 | log.Println("request") 180 | for i := 0; i < 1000; i++ { 181 | fmt.Fprintf(w, "Hello") 182 | } 183 | }) 184 | 185 | ocHandler := &ochttp.Handler{Handler: handler, IsPublicEndpoint: true} 186 | http.ListenAndServe(":8089", ocHandler) 187 | ``` 188 | 189 | ### Mongo 190 | With MongoDB wrapper you have to use a context, do don't create a new one and use the first one created 191 | 192 | [OpenCensus Documentation about mongowrapper](https://opencensus.io/guides/integrations/mongodb/go_driver/) 193 | 194 | ### SQL 195 | You can use it by registration or wrapping, here we use the first one 196 | 197 | NB: if you have some ocsql.warning, try to update your dependency 198 | 199 | [OpenCensus Documentation about ocsql](https://opencensus.io/guides/integrations/sql/go_sql/) 200 | 201 | ```golang 202 | import ( 203 | "database/sql" 204 | 205 | "contrib.go.opencensus.io/integrations/ocsql" 206 | ) 207 | 208 | // Create a new driver registred with OpenCensus 209 | driverName, err := ocsql.Register("postgres", ocsql.WithOptions(ocsql.TraceOptions{ 210 | AllowRoot: true, 211 | Ping: true, 212 | RowsNext: false, 213 | RowsClose: false, 214 | RowsAffected: true, 215 | LastInsertID: true, 216 | Query: true, 217 | QueryParams: false, // Don't send value of $1, $2... args 218 | })) 219 | if err != nil { 220 | log.Fatalf("Failed to register the ocsql driver: %v", err) 221 | return 222 | } 223 | 224 | // Then use the OpenCensus driver as usual 225 | db, err := sql.Open(driverName, fmt.Sprintf( 226 | "user=%s password=%s dbname=%s host=%s port=%s sslmode=disable", 227 | cfg.User, cfg.Password, cfg.Database, cfg.Host, cfg.Port)) 228 | if err != nil { 229 | err = errors.Wrapf(err, 230 | "Couldn't open connection to postgre database", 231 | ) 232 | return 233 | } 234 | 235 | // Use QueryContext for each query 236 | func DeleteThings(ctx context.Context) error { 237 | q, err := db.QueryContext(ctx, "DROP TABLE things;") 238 | q.Close() 239 | return err 240 | } 241 | ``` 242 | 243 | ### Other integrations 244 | We didn't test others integrations, but everything compatible with OpenCensus/Zipkin should work without any problem 245 | 246 | ## Known problems 247 | ### x509: unknown authority 248 | You must have the **Let's Encrypt Authority X3** certificate on your machine/container to connect to our backend 249 | 250 | #### Alpine based container 251 | ```sh 252 | apk add -U --no-cache ca-certificates 253 | ``` 254 | 255 | #### Debian based container 256 | ```sh 257 | apt-get install -y ca-certificates 258 | ``` -------------------------------------------------------------------------------- /assets/gopher-pm2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keymetrics/pm2-io-apm-go/63b337a500c597d1088c15c0cff28d981ecc4b6a/assets/gopher-pm2.png -------------------------------------------------------------------------------- /features/metrics/internal.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/keymetrics/pm2-io-apm-go/structures" 7 | ) 8 | 9 | // MetricsMemStats is a structure to simplify storage of mem values 10 | type MetricsMemStats struct { 11 | Initied bool 12 | NumGC *structures.Metric 13 | LastNumGC float64 14 | 15 | NumMallocs *structures.Metric 16 | LastNumMallocs float64 17 | 18 | NumFree *structures.Metric 19 | LastNumFree float64 20 | 21 | HeapAlloc *structures.Metric 22 | 23 | Pause *structures.Metric 24 | LastPause float64 25 | } 26 | 27 | // GlobalMetricsMemStats store current and last mem stats 28 | var GlobalMetricsMemStats MetricsMemStats 29 | 30 | // GoRoutines create a func metric who return number of current GoRoutines 31 | func GoRoutines() *structures.Metric { 32 | metric := structures.CreateFuncMetric("GoRoutines", "metric", "routines", func() float64 { 33 | return float64(runtime.NumGoroutine()) 34 | }) 35 | return &metric 36 | } 37 | 38 | // CgoCalls create a func metric who return number of current C calls of last second 39 | func CgoCalls() *structures.Metric { 40 | last := runtime.NumCgoCall() 41 | metric := structures.CreateFuncMetric("CgoCalls/sec", "metric", "calls/sec", func() float64 { 42 | calls := runtime.NumCgoCall() 43 | v := calls - last 44 | last = calls 45 | return float64(v) 46 | }) 47 | return &metric 48 | } 49 | 50 | // InitMetricsMemStats create metrics for MemStats 51 | func InitMetricsMemStats() { 52 | numGC := structures.CreateMetric("GCRuns/sec", "metric", "runs") 53 | numMalloc := structures.CreateMetric("mallocs/sec", "metric", "mallocs") 54 | numFree := structures.CreateMetric("free/sec", "metric", "frees") 55 | heapAlloc := structures.CreateMetric("heapAlloc", "metric", "bytes") 56 | pause := structures.CreateMetric("Pause/sec", "metric", "ns/sec") 57 | 58 | GlobalMetricsMemStats = MetricsMemStats{ 59 | Initied: true, 60 | NumGC: &numGC, 61 | NumMallocs: &numMalloc, 62 | NumFree: &numFree, 63 | HeapAlloc: &heapAlloc, 64 | Pause: &pause, 65 | } 66 | } 67 | 68 | // Handler write values in MemStats metrics 69 | func Handler() { 70 | var stats runtime.MemStats 71 | runtime.ReadMemStats(&stats) 72 | 73 | GlobalMetricsMemStats.NumGC.Set(float64(stats.NumGC) - GlobalMetricsMemStats.LastNumGC) 74 | GlobalMetricsMemStats.LastNumGC = float64(stats.NumGC) 75 | 76 | GlobalMetricsMemStats.NumMallocs.Set(float64(stats.Mallocs) - GlobalMetricsMemStats.LastNumMallocs) 77 | GlobalMetricsMemStats.LastNumMallocs = float64(stats.Mallocs) 78 | 79 | GlobalMetricsMemStats.NumFree.Set(float64(stats.Frees) - GlobalMetricsMemStats.LastNumFree) 80 | GlobalMetricsMemStats.LastNumFree = float64(stats.Frees) 81 | 82 | GlobalMetricsMemStats.HeapAlloc.Set(float64(stats.HeapAlloc)) 83 | 84 | GlobalMetricsMemStats.Pause.Set(float64(stats.PauseTotalNs) - GlobalMetricsMemStats.LastPause) 85 | GlobalMetricsMemStats.LastPause = float64(stats.PauseTotalNs) 86 | } 87 | -------------------------------------------------------------------------------- /features/metrics/internal_test.go: -------------------------------------------------------------------------------- 1 | package metrics_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keymetrics/pm2-io-apm-go/structures" 7 | 8 | "github.com/keymetrics/pm2-io-apm-go/features/metrics" 9 | ) 10 | 11 | func TestInternal(t *testing.T) { 12 | var goRoutines *structures.Metric 13 | t.Run("Get correct structures of goroutines", func(t *testing.T) { 14 | goRoutines = metrics.GoRoutines() 15 | }) 16 | 17 | t.Run("Should get a value", func(t *testing.T) { 18 | v := goRoutines.Get() 19 | if v == 0 { 20 | t.Fatal("Metric shouldn't be 0") 21 | } 22 | }) 23 | 24 | t.Run("Should init metrics stats", func(t *testing.T) { 25 | metrics.InitMetricsMemStats() 26 | }) 27 | 28 | t.Run("should run handler without problem", func(t *testing.T) { 29 | metrics.Handler() 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /features/metrics/system.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "time" 7 | 8 | "github.com/pbnjay/memory" 9 | "github.com/shirou/gopsutil/cpu" 10 | "github.com/shirou/gopsutil/load" 11 | "github.com/shirou/gopsutil/process" 12 | ) 13 | 14 | var lastCPUTotal float64 15 | 16 | // CPUPercent return current CPU usage 17 | func CPUPercent() (float64, error) { 18 | p, err := process.NewProcess(int32(os.Getpid())) 19 | if err != nil { 20 | return 0, err 21 | } 22 | crtTime, err := p.CreateTime() 23 | if err != nil { 24 | return 0, err 25 | } 26 | 27 | cput, err := p.Times() 28 | if err != nil { 29 | return 0, err 30 | } 31 | 32 | created := time.Unix(0, crtTime*int64(time.Millisecond)) 33 | totalTime := time.Since(created).Seconds() 34 | 35 | if totalTime <= 0 { 36 | return 0, nil 37 | } 38 | 39 | val := (cput.Total() - lastCPUTotal) * 100 40 | lastCPUTotal = cput.Total() 41 | 42 | return val, nil 43 | } 44 | 45 | // CPUName return first CPU name 46 | func CPUName() string { 47 | infos, err := cpu.Info() 48 | if err != nil { 49 | return "" 50 | } 51 | return infos[0].ModelName 52 | } 53 | 54 | // CPULoad return load1, load5, load15 55 | func CPULoad() []float64 { 56 | avg, err := load.Avg() 57 | if err != nil { 58 | return []float64{} 59 | } 60 | return []float64{avg.Load1, avg.Load5, avg.Load15} 61 | } 62 | 63 | // TotalMem return total memory in bytes 64 | func TotalMem() uint64 { 65 | return memory.TotalMemory() 66 | } 67 | 68 | // LocalIP return ip of first interface 69 | func LocalIP() string { 70 | addrs, err := net.InterfaceAddrs() 71 | if err != nil { 72 | return "" 73 | } 74 | for _, address := range addrs { 75 | // check the address type and if it is not a loopback the display it 76 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 77 | if ipnet.IP.To16() != nil { 78 | return ipnet.IP.String() 79 | } 80 | } 81 | } 82 | return "" 83 | } 84 | -------------------------------------------------------------------------------- /features/metrics/system_test.go: -------------------------------------------------------------------------------- 1 | package metrics_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keymetrics/pm2-io-apm-go/features/metrics" 7 | ) 8 | 9 | func TestSystem(t *testing.T) { 10 | t.Run("Get cpu percent value", func(t *testing.T) { 11 | value, err := metrics.CPUPercent() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | if value < 0 { 16 | t.Fatal("value less than 0") 17 | } 18 | }) 19 | 20 | t.Run("Get cpu name", func(t *testing.T) { 21 | value := metrics.CPUName() 22 | if len(value) == 0 { 23 | t.Fatal("name empty") 24 | } 25 | }) 26 | 27 | t.Run("Get cpu load", func(t *testing.T) { 28 | value := metrics.CPULoad() 29 | if len(value) != 3 { 30 | t.Fatal("load empty") 31 | } 32 | }) 33 | 34 | t.Run("Get local ip", func(t *testing.T) { 35 | value := metrics.LocalIP() 36 | if len(value) == 0 { 37 | t.Fatal("ip empty") 38 | } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /features/notifier.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/keymetrics/pm2-io-apm-go/services" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // Notifier with transporter 11 | type Notifier struct { 12 | Transporter *services.Transporter 13 | } 14 | 15 | // Error packet to KM 16 | type Error struct { 17 | Message string `json:"message"` 18 | Stack string `json:"stack"` 19 | } 20 | 21 | type stackTracer interface { 22 | StackTrace() errors.StackTrace 23 | } 24 | 25 | // Error to KM 26 | func (notifier *Notifier) Error(err error) { 27 | stack := "" 28 | if err, ok := err.(stackTracer); ok { 29 | for _, f := range err.StackTrace() { 30 | stack += fmt.Sprintf("%+v", f) 31 | } 32 | } else { 33 | stack = fmt.Sprintf("%+v", err) 34 | } 35 | notifier.Transporter.Send("process:exception", Error{ 36 | Message: err.Error(), 37 | Stack: stack, 38 | }) 39 | } 40 | 41 | // Log to KM 42 | func (notifier *Notifier) Log(log string) { 43 | notifier.Transporter.Send("logs", log) 44 | } 45 | -------------------------------------------------------------------------------- /features/profiling_test.go: -------------------------------------------------------------------------------- 1 | package features_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keymetrics/pm2-io-apm-go/features" 7 | ) 8 | 9 | func TestProfiling(t *testing.T) { 10 | t.Run("Should start CPU profiling", func(t *testing.T) { 11 | err := features.StartCPUProfile() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | }) 16 | 17 | t.Run("Should stop CPU profiling", func(t *testing.T) { 18 | res, err := features.StopCPUProfile() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if len(res) == 0 { 23 | t.Fatal("result is empty") 24 | } 25 | }) 26 | 27 | t.Run("Should heapdump", func(t *testing.T) { 28 | res, err := features.HeapDump() 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | if len(res) == 0 { 33 | t.Fatal("result is empty") 34 | } 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /features/profling.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "io/ioutil" 5 | "runtime/pprof" 6 | ) 7 | 8 | var currentPath string 9 | 10 | // HeapDump return a binary string of a pprof 11 | func HeapDump() ([]byte, error) { 12 | f, err := ioutil.TempFile("/tmp", "heapdump") 13 | if err != nil { 14 | return nil, err 15 | } 16 | err = pprof.WriteHeapProfile(f) 17 | if err != nil { 18 | return nil, err 19 | } 20 | r, err := ioutil.ReadFile(f.Name()) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return r, nil 25 | } 26 | 27 | // StartCPUProfile start a CPU profiling and write it to file 28 | func StartCPUProfile() error { 29 | f, err := ioutil.TempFile("/tmp", "cpuprofile") 30 | if err != nil { 31 | return err 32 | } 33 | currentPath = f.Name() 34 | return pprof.StartCPUProfile(f) 35 | } 36 | 37 | // StopCPUProfile stop the profiling and read the file 38 | func StopCPUProfile() ([]byte, error) { 39 | pprof.StopCPUProfile() 40 | r, err := ioutil.ReadFile(currentPath) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return r, nil 45 | } 46 | -------------------------------------------------------------------------------- /features/tracing.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "github.com/keymetrics/pm2-io-apm-go/services" 5 | "github.com/keymetrics/pm2-io-apm-go/structures" 6 | openzipkin "github.com/openzipkin/zipkin-go" 7 | "github.com/openzipkin/zipkin-go/model" 8 | "github.com/openzipkin/zipkin-go/reporter" 9 | "go.opencensus.io/exporter/zipkin" 10 | "go.opencensus.io/trace" 11 | ) 12 | 13 | // InitTracing with config and transporter provided 14 | func InitTracing(config *structures.Config, transporter *services.Transporter) error { 15 | endpoint, err := openzipkin.NewEndpoint(config.Name, "") 16 | if err != nil { 17 | return err 18 | } 19 | 20 | // OpenCensus 21 | ze := zipkin.NewExporter(NewWsReporter(transporter), endpoint) 22 | trace.RegisterExporter(ze) 23 | trace.ApplyConfig(trace.Config{DefaultSampler: trace.ProbabilitySampler(0.5)}) 24 | 25 | return nil 26 | } 27 | 28 | // WsReporter is a Zipkin compatible reporter through PM2 WebSocket 29 | type WsReporter struct { 30 | Transporter *services.Transporter 31 | } 32 | 33 | // Send a message using PM2 transporter 34 | func (r *WsReporter) Send(s model.SpanModel) { 35 | type Alias model.SpanModel 36 | 37 | timers, err := getTimers(s) 38 | if err != nil { 39 | return 40 | } 41 | 42 | t := &struct { 43 | Process structures.Process `json:"process"` 44 | Alias 45 | Timers 46 | }{ 47 | Process: structures.Process{ 48 | PmID: 0, 49 | Name: r.Transporter.Config.Name, 50 | Server: r.Transporter.Config.ServerName, 51 | }, 52 | Alias: (Alias)(s), 53 | Timers: *timers, 54 | } 55 | msg := services.Message{ 56 | Channel: "trace-span", 57 | Payload: t, 58 | } 59 | r.Transporter.SendJson(msg) 60 | } 61 | 62 | // Close the reporter (not used, ws handled in transporter) 63 | func (r *WsReporter) Close() error { 64 | return nil 65 | } 66 | 67 | // NewWsReporter create a reporter using specified transporter 68 | func NewWsReporter(transporter *services.Transporter) reporter.Reporter { 69 | rep := WsReporter{ 70 | Transporter: transporter, 71 | } 72 | return &rep 73 | } 74 | -------------------------------------------------------------------------------- /features/tracing_model.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/openzipkin/zipkin-go/model" 8 | ) 9 | 10 | // Timers store timestamp and duration in JSON format from model.SpanModel 11 | type Timers struct { 12 | T int64 `json:"timestamp,omitempty"` 13 | D int64 `json:"duration,omitempty"` 14 | } 15 | 16 | func getTimers(s model.SpanModel) (*Timers, error) { 17 | var timestamp int64 18 | if !s.Timestamp.IsZero() { 19 | if s.Timestamp.Unix() < 1 { 20 | // Zipkin does not allow Timestamps before Unix epoch 21 | return nil, errors.New("ErrValidTimestampRequired") 22 | } 23 | timestamp = s.Timestamp.Round(time.Microsecond).UnixNano() / 1e3 24 | } 25 | 26 | if s.Duration < time.Microsecond { 27 | if s.Duration < 0 { 28 | // negative duration is not allowed and signals a timing logic error 29 | return nil, errors.New("ErrValidDurationRequired") 30 | } else if s.Duration > 0 { 31 | // sub microsecond durations are reported as 1 microsecond 32 | s.Duration = 1 * time.Microsecond 33 | } 34 | } else { 35 | // Duration will be rounded to nearest microsecond representation. 36 | // 37 | // NOTE: Duration.Round() is not available in Go 1.8 which we still support. 38 | // To handle microsecond resolution rounding we'll add 500 nanoseconds to 39 | // the duration. When truncated to microseconds in the call to marshal, it 40 | // will be naturally rounded. See TestSpanDurationRounding in span_test.go 41 | s.Duration += 500 * time.Nanosecond 42 | } 43 | 44 | if s.LocalEndpoint.Empty() { 45 | s.LocalEndpoint = nil 46 | } 47 | 48 | if s.RemoteEndpoint.Empty() { 49 | s.RemoteEndpoint = nil 50 | } 51 | 52 | timers := Timers{ 53 | T: timestamp, 54 | D: s.Duration.Nanoseconds() / 1e3, 55 | } 56 | 57 | return &timers, nil 58 | } 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keymetrics/pm2-io-apm-go 2 | 3 | require ( 4 | github.com/google/uuid v1.1.1 5 | github.com/gorilla/websocket v1.4.0 6 | github.com/openzipkin/zipkin-go v0.1.5 7 | github.com/pbnjay/memory v0.0.0-20190104145345-974d429e7ae4 8 | github.com/pkg/errors v0.8.1 9 | github.com/shirou/gopsutil v2.18.12+incompatible 10 | go.opencensus.io v0.19.0 11 | gopkg.in/h2non/gock.v1 v1.0.14 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | git.apache.org/thrift.git v0.0.0-20181218151757-9b75e4fe745a/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 5 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 6 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 7 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 10 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 11 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 13 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 14 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 15 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 16 | github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 17 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 18 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 19 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 21 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 22 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 23 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 25 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 26 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 27 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 28 | github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 29 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 30 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 31 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 32 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 33 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 34 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 35 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 36 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 37 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 38 | github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 39 | github.com/openzipkin/zipkin-go v0.1.5 h1:rLsvZsPv77FANOKwS9Ks8izXDqnISDVvml7AO6qRJps= 40 | github.com/openzipkin/zipkin-go v0.1.5/go.mod h1:8NDCjKHoHW1XOp/vf3lClHem0b91r4433B67KXyKXAQ= 41 | github.com/pbnjay/memory v0.0.0-20190104145345-974d429e7ae4 h1:MfIUBZ1bz7TgvQLVa/yPJZOGeKEgs6eTKUjz3zB4B+U= 42 | github.com/pbnjay/memory v0.0.0-20190104145345-974d429e7ae4/go.mod h1:RMU2gJXhratVxBDTFeOdNhd540tG57lt9FIUV0YLvIQ= 43 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 44 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 45 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 46 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 47 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 48 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 49 | github.com/prometheus/common v0.0.0-20181218105931-67670fe90761/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 50 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 51 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 52 | github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM= 53 | github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 54 | go.opencensus.io v0.19.0 h1:+jrnNy8MR4GZXvwF9PEuSyHxA4NaTf6601oNRwCSXq0= 55 | go.opencensus.io v0.19.0/go.mod h1:AYeH0+ZxYyghG8diqaaIq/9P3VgCCt5GF2ldCY4dkFg= 56 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 57 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 58 | golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 59 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 60 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 61 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 62 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 63 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 64 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 65 | golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 66 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 67 | golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 68 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 69 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20181218192612-074acd46bca6 h1:MXtOG7w2ND9qNCUZSDBGll/SpVIq7ftozR9I8/JGBHY= 75 | golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 76 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 77 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 79 | golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 80 | google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 81 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 82 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 83 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 84 | google.golang.org/genproto v0.0.0-20181109154231-b5d43981345b/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 85 | google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 86 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 87 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 88 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 89 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 90 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 91 | gopkg.in/h2non/gock.v1 v1.0.14 h1:fTeu9fcUvSnLNacYvYI54h+1/XEteDyHvrVCZEEEYNM= 92 | gopkg.in/h2non/gock.v1 v1.0.14/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= 93 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 94 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 95 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 96 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 97 | honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 98 | -------------------------------------------------------------------------------- /pm2io.go: -------------------------------------------------------------------------------- 1 | package pm2io 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | "time" 7 | 8 | "github.com/keymetrics/pm2-io-apm-go/features" 9 | "github.com/keymetrics/pm2-io-apm-go/features/metrics" 10 | "github.com/keymetrics/pm2-io-apm-go/services" 11 | "github.com/keymetrics/pm2-io-apm-go/structures" 12 | "github.com/shirou/gopsutil/process" 13 | ) 14 | 15 | var version = "0.0.1" 16 | 17 | // Pm2Io to config and access all services 18 | type Pm2Io struct { 19 | Config *structures.Config 20 | 21 | Notifier *features.Notifier 22 | transporter *services.Transporter 23 | 24 | StatusOverrider func() *structures.Status 25 | 26 | startTime time.Time 27 | } 28 | 29 | // Start and prepare services + profiling 30 | func (pm2io *Pm2Io) Start() { 31 | pm2io.Config.InitNames() 32 | 33 | defaultNode := "api.cloud.pm2.io" 34 | if pm2io.Config.Node == nil { 35 | pm2io.Config.Node = &defaultNode 36 | } 37 | 38 | pm2io.transporter = services.NewTransporter(pm2io.Config, version) 39 | pm2io.Notifier = &features.Notifier{ 40 | Transporter: pm2io.transporter, 41 | } 42 | 43 | pm2io.prepareMetrics() 44 | pm2io.prepareProfiling() 45 | 46 | pm2io.startTime = time.Now() 47 | pm2io.transporter.Connect() 48 | 49 | go func() { 50 | ticker := time.NewTicker(time.Second) 51 | for { 52 | <-ticker.C 53 | pm2io.SendStatus() 54 | } 55 | }() 56 | } 57 | 58 | // RestartTransporter including webSocket connection 59 | func (pm2io *Pm2Io) RestartTransporter() { 60 | pm2io.transporter.CloseAndReconnect() 61 | } 62 | 63 | // SendStatus of current state 64 | func (pm2io *Pm2Io) SendStatus() { 65 | if pm2io.StatusOverrider != nil { 66 | pm2io.transporter.Send("status", pm2io.StatusOverrider()) 67 | return 68 | } 69 | 70 | pm2io.transporter.Send("status", structures.Status{ 71 | Process: []structures.StatusProcess{pm2io.getProcess()}, 72 | Server: pm2io.getServer(), 73 | }) 74 | } 75 | 76 | // Panic notify KM then panic 77 | func (pm2io *Pm2Io) Panic(err error) { 78 | pm2io.Notifier.Error(err) 79 | panic(err) 80 | } 81 | 82 | // StartTracing add global handlers for OpenCensus providers 83 | func (pm2io *Pm2Io) StartTracing() error { 84 | return features.InitTracing(pm2io.Config, pm2io.transporter) 85 | } 86 | 87 | func (pm2io *Pm2Io) prepareMetrics() { 88 | metrics.InitMetricsMemStats() 89 | services.AttachHandler(metrics.Handler) 90 | services.AddMetric(metrics.GoRoutines()) 91 | services.AddMetric(metrics.CgoCalls()) 92 | services.AddMetric(metrics.GlobalMetricsMemStats.NumGC) 93 | services.AddMetric(metrics.GlobalMetricsMemStats.NumMallocs) 94 | services.AddMetric(metrics.GlobalMetricsMemStats.NumFree) 95 | services.AddMetric(metrics.GlobalMetricsMemStats.HeapAlloc) 96 | services.AddMetric(metrics.GlobalMetricsMemStats.Pause) 97 | } 98 | 99 | func (pm2io *Pm2Io) prepareProfiling() { 100 | services.AddAction(&structures.Action{ 101 | ActionName: "km:heapdump", 102 | ActionType: "internal", 103 | Callback: func(payload map[string]interface{}) string { 104 | data, err := features.HeapDump() 105 | if err != nil { 106 | pm2io.Notifier.Error(err) 107 | } 108 | pm2io.transporter.Send("profilings", structures.NewProfilingResponse(data, "heapdump")) 109 | return "" 110 | }, 111 | }) 112 | services.AddAction(&structures.Action{ 113 | ActionName: "km:cpu:profiling:start", 114 | ActionType: "internal", 115 | Callback: func(payload map[string]interface{}) string { 116 | err := features.StartCPUProfile() 117 | if err != nil { 118 | pm2io.Notifier.Error(err) 119 | } 120 | 121 | if payload["opts"] != nil { 122 | go func() { 123 | timeout := payload["opts"].(map[string]interface{})["timeout"] 124 | time.Sleep(time.Duration(timeout.(float64)) * time.Millisecond) 125 | r, err := features.StopCPUProfile() 126 | if err != nil { 127 | pm2io.Notifier.Error(err) 128 | } 129 | pm2io.transporter.Send("profilings", structures.NewProfilingResponse(r, "cpuprofile")) 130 | }() 131 | } 132 | return "" 133 | }, 134 | }) 135 | services.AddAction(&structures.Action{ 136 | ActionName: "km:cpu:profiling:stop", 137 | ActionType: "internal", 138 | Callback: func(payload map[string]interface{}) string { 139 | r, err := features.StopCPUProfile() 140 | if err != nil { 141 | pm2io.Notifier.Error(err) 142 | } 143 | pm2io.transporter.Send("profilings", structures.NewProfilingResponse(r, "cpuprofile")) 144 | return "" 145 | }, 146 | }) 147 | } 148 | 149 | func (pm2io *Pm2Io) getServer() structures.Server { 150 | return structures.Server{ 151 | Loadavg: metrics.CPULoad(), 152 | TotalMem: metrics.TotalMem(), 153 | Hostname: pm2io.Config.Hostname, 154 | Uptime: (time.Now().UnixNano()-pm2io.startTime.UnixNano())/int64(time.Millisecond) + 600000, 155 | Pm2Version: version, 156 | Type: runtime.GOOS, 157 | Interaction: true, 158 | CPU: structures.CPU{ 159 | Number: runtime.NumCPU(), 160 | Info: metrics.CPUName(), 161 | }, 162 | NodeVersion: runtime.Version(), 163 | } 164 | } 165 | 166 | func (pm2io *Pm2Io) getProcess() structures.StatusProcess { 167 | p, err := process.NewProcess(int32(os.Getpid())) 168 | if err != nil { 169 | pm2io.Notifier.Error(err) 170 | } 171 | cp, err := metrics.CPUPercent() 172 | if err != nil { 173 | pm2io.Notifier.Error(err) 174 | } 175 | 176 | return structures.StatusProcess{ 177 | Pid: p.Pid, 178 | Name: pm2io.Config.Name, 179 | Interpreter: "golang", 180 | RestartTime: 0, 181 | CreatedAt: pm2io.startTime.UnixNano() / int64(time.Millisecond), 182 | ExecMode: "fork_mode", 183 | PmUptime: pm2io.startTime.UnixNano() / int64(time.Millisecond), 184 | Status: "online", 185 | PmID: 0, 186 | CPU: cp, 187 | Memory: uint64(metrics.GlobalMetricsMemStats.HeapAlloc.Value), 188 | UniqueID: pm2io.Config.ProcessUniqueID, 189 | AxmActions: services.Actions, 190 | AxmMonitor: services.GetMetricsAsMap(), 191 | AxmOptions: structures.Options{ 192 | HeapDump: true, 193 | Profiling: true, 194 | CustomProbes: true, 195 | Apm: structures.Apm{ 196 | Type: "golang", 197 | Version: version, 198 | }, 199 | }, 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /services/action.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "github.com/keymetrics/pm2-io-apm-go/structures" 4 | 5 | var Actions []*structures.Action 6 | 7 | // AddAction add an action to global Actions array 8 | func AddAction(action *structures.Action) { 9 | Actions = append(Actions, action) 10 | } 11 | 12 | // CallAction with specific name (like pmx) 13 | func CallAction(name string, payload map[string]interface{}) *string { 14 | for _, i := range Actions { 15 | if i.ActionName == name { 16 | response := i.Callback(payload) 17 | return &response 18 | } 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /services/action_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keymetrics/pm2-io-apm-go/services" 7 | "github.com/keymetrics/pm2-io-apm-go/structures" 8 | ) 9 | 10 | func TestAction(t *testing.T) { 11 | var action structures.Action 12 | 13 | t.Run("Create action", func(t *testing.T) { 14 | action = structures.Action{ 15 | ActionName: "MyAction", 16 | Callback: func(_ map[string]interface{}) string { 17 | return "GOOD" 18 | }, 19 | } 20 | }) 21 | 22 | t.Run("Add action to service", func(t *testing.T) { 23 | services.AddAction(&action) 24 | }) 25 | 26 | t.Run("Must return correct value", func(t *testing.T) { 27 | resp := services.CallAction("MyAction", map[string]interface{}{}) 28 | if resp == nil { 29 | t.Fatal("response is nil") 30 | } 31 | }) 32 | 33 | t.Run("Must return nil for unknown action call", func(t *testing.T) { 34 | resp := services.CallAction("Unknown", map[string]interface{}{}) 35 | if resp != nil { 36 | t.Fatal("response is not nil") 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /services/metric.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "github.com/keymetrics/pm2-io-apm-go/structures" 4 | 5 | var Metrics []*structures.Metric 6 | var handler *func() 7 | 8 | // AddMetric to global metrics array 9 | func AddMetric(metric *structures.Metric) { 10 | Metrics = append(Metrics, metric) 11 | } 12 | 13 | // GetMetricsAsMap prepare metrics before sending 14 | func GetMetricsAsMap() map[string]*structures.Metric { 15 | if handler != nil { 16 | (*handler)() 17 | } 18 | 19 | m := make(map[string]*structures.Metric, len(Metrics)) 20 | for _, metric := range Metrics { 21 | metric.Get() 22 | m[metric.Name] = metric 23 | } 24 | return m 25 | } 26 | 27 | // AttachHandler add a handler for each loop before preparing metrics 28 | func AttachHandler(newHandler func()) { 29 | handler = &newHandler 30 | } 31 | -------------------------------------------------------------------------------- /services/metric_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keymetrics/pm2-io-apm-go/structures" 7 | 8 | "github.com/keymetrics/pm2-io-apm-go/services" 9 | ) 10 | 11 | func TestMetrics(t *testing.T) { 12 | var handlerCalled = false 13 | stdMetric := structures.CreateMetric("name", "category", "unit") 14 | 15 | t.Run("Generate map", func(t *testing.T) { 16 | metrics := services.GetMetricsAsMap() 17 | if metrics == nil { 18 | t.Fatal("cannot get map") 19 | } 20 | }) 21 | 22 | t.Run("Can add handler", func(t *testing.T) { 23 | services.AttachHandler(func() { 24 | handlerCalled = true 25 | }) 26 | }) 27 | 28 | t.Run("Check if handler called", func(t *testing.T) { 29 | metrics := services.GetMetricsAsMap() 30 | if metrics == nil { 31 | t.Fatal("cannot get map") 32 | } 33 | 34 | if !handlerCalled { 35 | t.Fatal("Handler attached but never called") 36 | } 37 | }) 38 | 39 | t.Run("add metric", func(t *testing.T) { 40 | services.AddMetric(&stdMetric) 41 | }) 42 | 43 | t.Run("should be in array", func(t *testing.T) { 44 | if len(services.Metrics) != 1 { 45 | t.Fatal("wanted 1 metric, got " + string(len(services.Metrics))) 46 | } 47 | }) 48 | 49 | t.Run("should be in map", func(t *testing.T) { 50 | metrics := services.GetMetricsAsMap() 51 | if metrics == nil { 52 | t.Fatal("cannot get map") 53 | } 54 | if metrics["name"] == nil { 55 | t.Fatal("not inserted") 56 | } 57 | if metrics["name"].Unit != "unit" { 58 | t.Fatal("metric data problem") 59 | } 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /services/structure.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "github.com/keymetrics/pm2-io-apm-go/structures" 4 | 5 | // PayLoad is structure for receiving json data 6 | type PayLoad struct { 7 | At int64 `json:"at"` 8 | Data interface{} `json:"data"` 9 | Process structures.Process `json:"process,omitempty"` 10 | Active bool `json:"active"` 11 | ServerName string `json:"server_name"` 12 | InternalIP string `json:"internal_ip"` 13 | Protected bool `json:"protected"` 14 | RevCon bool `json:"rev_con"` 15 | } 16 | 17 | // Verify is the object send to api.cloud.pm2.io to get a node to connect 18 | type Verify struct { 19 | PublicId string `json:"public_id"` 20 | PrivateId string `json:"private_id"` 21 | Data VerifyData `json:"data"` 22 | } 23 | 24 | // VerifyData is a part of Verify 25 | type VerifyData struct { 26 | MachineName string `json:"MACHINE_NAME"` 27 | Cpus int `json:"CPUS"` //nb thread 28 | Memory uint64 `json:"MEMORY"` //bytes 29 | Pm2Version string `json:"PM2_VERSION"` 30 | Hostname string `json:"HOSTNAME"` 31 | } 32 | 33 | // VerifyResponse is the object sent by api.cloud.pm2.io 34 | type VerifyResponse struct { 35 | Endpoints Endpoints `json:"endpoints"` 36 | } 37 | 38 | // Endpoints list of api.cloud.pm2.io 39 | type Endpoints struct { 40 | WS string `json:"ws"` 41 | } 42 | -------------------------------------------------------------------------------- /services/transport.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "runtime" 11 | "sync" 12 | "time" 13 | 14 | "github.com/gorilla/websocket" 15 | "github.com/keymetrics/pm2-io-apm-go/features/metrics" 16 | "github.com/keymetrics/pm2-io-apm-go/structures" 17 | ) 18 | 19 | // Transporter handle, send and receive packet from KM 20 | type Transporter struct { 21 | Config *structures.Config 22 | Version string 23 | 24 | ws *websocket.Conn 25 | mu sync.Mutex 26 | isConnected bool 27 | isHandling bool 28 | isConnecting bool 29 | isClosed bool 30 | wsNode *string 31 | heartbeatTicker *time.Ticker // 5 seconds 32 | serverTicker *time.Ticker // 10 minutes 33 | } 34 | 35 | // Message receive from KM 36 | type Message struct { 37 | Payload interface{} `json:"payload"` 38 | Channel string `json:"channel"` 39 | } 40 | 41 | // NewTransporter with default values 42 | func NewTransporter(config *structures.Config, version string) *Transporter { 43 | return &Transporter{ 44 | Config: config, 45 | Version: version, 46 | 47 | isHandling: false, 48 | isConnecting: false, 49 | isClosed: false, 50 | isConnected: false, 51 | } 52 | } 53 | 54 | // GetServer check api.cloud.pm2.io to get current node 55 | func (transporter *Transporter) GetServer() *string { 56 | verify := Verify{ 57 | PublicId: transporter.Config.PublicKey, 58 | PrivateId: transporter.Config.PrivateKey, 59 | Data: VerifyData{ 60 | MachineName: transporter.Config.ServerName, 61 | Cpus: runtime.NumCPU(), 62 | Memory: metrics.TotalMem(), 63 | Pm2Version: transporter.Version, 64 | Hostname: transporter.Config.Hostname, 65 | }, 66 | } 67 | jsonValue, _ := json.Marshal(verify) 68 | 69 | res, err := transporter.httpClient().Post("https://"+*transporter.Config.Node+"/api/node/verifyPM2", "application/json", bytes.NewBuffer(jsonValue)) 70 | if err != nil { 71 | log.Println("Error while trying to get endpoints (verifypm2)", err) 72 | return nil 73 | } 74 | data, err := ioutil.ReadAll(res.Body) 75 | if err != nil { 76 | return nil 77 | } 78 | res.Body.Close() 79 | response := VerifyResponse{} 80 | err = json.Unmarshal(data, &response) 81 | if err != nil { 82 | log.Println("PM2 server sent an incorrect response") 83 | return nil 84 | } 85 | return &response.Endpoints.WS 86 | } 87 | 88 | // Connect to current wsNode with headers and prepare handlers/tickers 89 | func (transporter *Transporter) Connect() { 90 | if transporter.wsNode == nil { 91 | transporter.wsNode = transporter.GetServer() 92 | } 93 | if transporter.wsNode == nil { 94 | go func() { 95 | log.Println("Cannot get node, retry in 10sec") 96 | time.Sleep(10 * time.Second) 97 | transporter.Connect() 98 | }() 99 | return 100 | } 101 | 102 | headers := http.Header{} 103 | headers.Add("X-KM-PUBLIC", transporter.Config.PublicKey) 104 | headers.Add("X-KM-SECRET", transporter.Config.PrivateKey) 105 | headers.Add("X-KM-SERVER", transporter.Config.ServerName) 106 | headers.Add("X-PM2-VERSION", transporter.Version) 107 | headers.Add("X-PROTOCOL-VERSION", "1") 108 | headers.Add("User-Agent", "PM2 Agent Golang v"+transporter.Version) 109 | 110 | c, _, err := transporter.websocketDialer().Dial(*transporter.wsNode, headers) 111 | if err != nil { 112 | log.Println("Error while connecting to WS", err) 113 | time.Sleep(2 * time.Second) 114 | transporter.isConnecting = false 115 | transporter.CloseAndReconnect() 116 | return 117 | } 118 | c.SetCloseHandler(func(code int, text string) error { 119 | transporter.isClosed = true 120 | return nil 121 | }) 122 | 123 | transporter.isConnected = true 124 | transporter.isConnecting = false 125 | 126 | transporter.ws = c 127 | 128 | if !transporter.isHandling { 129 | transporter.SetHandlers() 130 | } 131 | 132 | go func() { 133 | if transporter.serverTicker != nil { 134 | return 135 | } 136 | transporter.serverTicker = time.NewTicker(10 * time.Minute) 137 | for { 138 | <-transporter.serverTicker.C 139 | srv := transporter.GetServer() 140 | if srv != nil && *srv != *transporter.wsNode { 141 | transporter.wsNode = srv 142 | transporter.CloseAndReconnect() 143 | } 144 | } 145 | }() 146 | } 147 | 148 | // SetHandlers for messages and pinger ticker 149 | func (transporter *Transporter) SetHandlers() { 150 | transporter.isHandling = true 151 | 152 | go transporter.MessagesHandler() 153 | 154 | go func() { 155 | if transporter.heartbeatTicker != nil { 156 | return 157 | } 158 | transporter.heartbeatTicker = time.NewTicker(5 * time.Second) 159 | for { 160 | <-transporter.heartbeatTicker.C 161 | transporter.mu.Lock() 162 | err := transporter.ws.WriteMessage(websocket.PingMessage, []byte{}) 163 | transporter.mu.Unlock() 164 | if err != nil { 165 | transporter.CloseAndReconnect() 166 | } 167 | } 168 | }() 169 | } 170 | 171 | func (transporter *Transporter) websocketDialer() (dialer *websocket.Dialer) { 172 | dialer = websocket.DefaultDialer 173 | if transporter.Config.Proxy == "" { 174 | return 175 | } 176 | url, err := url.Parse(transporter.Config.Proxy) 177 | if err != nil { 178 | log.Println("Proxy config incorrect, using default network for websocket", err) 179 | return 180 | } 181 | dialer.Proxy = http.ProxyURL(url) 182 | return 183 | } 184 | 185 | func (transporter *Transporter) httpClient() *http.Client { 186 | if transporter.Config.Proxy == "" { 187 | return &http.Client{} 188 | } 189 | url, err := url.Parse(transporter.Config.Proxy) 190 | if err != nil { 191 | log.Println("Proxy config incorrect, using default network for http", err) 192 | return &http.Client{} 193 | } 194 | return &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(url)}} 195 | } 196 | 197 | // MessagesHandler from KM 198 | func (transporter *Transporter) MessagesHandler() { 199 | for { 200 | _, message, err := transporter.ws.ReadMessage() 201 | if err != nil { 202 | transporter.isHandling = false 203 | transporter.CloseAndReconnect() 204 | return 205 | } 206 | 207 | var dat map[string]interface{} 208 | 209 | if err := json.Unmarshal(message, &dat); err != nil { 210 | panic(err) 211 | } 212 | 213 | if dat["channel"] == "trigger:action" { 214 | payload := dat["payload"].(map[string]interface{}) 215 | name := payload["action_name"] 216 | 217 | response := CallAction(name.(string), payload) 218 | 219 | transporter.Send("trigger:action:success", map[string]interface{}{ 220 | "success": true, 221 | "id": payload["process_id"], 222 | "action_name": name, 223 | }) 224 | transporter.Send("axm:reply", map[string]interface{}{ 225 | "action_name": name, 226 | "return": response, 227 | }) 228 | 229 | } else if dat["channel"] == "trigger:pm2:action" { 230 | payload := dat["payload"].(map[string]interface{}) 231 | name := payload["method_name"] 232 | switch name { 233 | case "startLogging": 234 | transporter.SendJson(map[string]interface{}{ 235 | "channel": "trigger:pm2:result", 236 | "payload": map[string]interface{}{ 237 | "ret": map[string]interface{}{ 238 | "err": nil, 239 | }, 240 | }, 241 | }) 242 | break 243 | } 244 | } else { 245 | log.Println("msg not registered: " + dat["channel"].(string)) 246 | } 247 | } 248 | } 249 | 250 | // SendJson marshal it and check errors 251 | func (transporter *Transporter) SendJson(msg interface{}) { 252 | b, err := json.Marshal(msg) 253 | if err != nil { 254 | return 255 | } 256 | 257 | transporter.mu.Lock() 258 | defer transporter.mu.Unlock() 259 | 260 | if !transporter.isConnected { 261 | return 262 | } 263 | transporter.ws.SetWriteDeadline(time.Now().Add(30 * time.Second)) 264 | err = transporter.ws.WriteMessage(websocket.TextMessage, b) 265 | if err != nil { 266 | transporter.CloseAndReconnect() 267 | } 268 | } 269 | 270 | // Send to specified channel 271 | func (transporter *Transporter) Send(channel string, data interface{}) { 272 | transporter.SendJson(Message{ 273 | Channel: channel, 274 | Payload: PayLoad{ 275 | At: time.Now().UnixNano() / int64(time.Millisecond), 276 | Process: structures.Process{ 277 | PmID: 0, 278 | Name: transporter.Config.Name, 279 | Server: transporter.Config.ServerName, // WARN: maybe error here 280 | }, 281 | Data: data, 282 | Active: true, 283 | ServerName: transporter.Config.ServerName, 284 | Protected: false, 285 | RevCon: true, 286 | InternalIP: metrics.LocalIP(), 287 | }, 288 | }) 289 | } 290 | 291 | // CloseAndReconnect webSocket 292 | func (transporter *Transporter) CloseAndReconnect() { 293 | if transporter.isConnecting { 294 | return 295 | } 296 | 297 | transporter.isConnecting = true 298 | if !transporter.isClosed && transporter.ws != nil { 299 | transporter.isConnected = false 300 | transporter.ws.Close() 301 | } 302 | transporter.Connect() 303 | } 304 | 305 | // IsConnected expose if everything is running normally 306 | func (transporter *Transporter) IsConnected() bool { 307 | return transporter.isConnected && transporter.isHandling && !transporter.isConnecting 308 | } 309 | 310 | // GetWsNode export current node 311 | func (transporter *Transporter) GetWsNode() *string { 312 | return transporter.wsNode 313 | } 314 | -------------------------------------------------------------------------------- /services/transport_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/httptest" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/gorilla/websocket" 14 | "github.com/keymetrics/pm2-io-apm-go/features/metrics" 15 | "github.com/keymetrics/pm2-io-apm-go/services" 16 | "github.com/keymetrics/pm2-io-apm-go/structures" 17 | gock "gopkg.in/h2non/gock.v1" 18 | ) 19 | 20 | var nbConnected int 21 | 22 | func TestTransport(t *testing.T) { 23 | var wssServer *httptest.Server 24 | var gockVerif *gock.Response 25 | 26 | var transporter *services.Transporter 27 | defaultNode := "api.cloud.pm2.io" 28 | 29 | t.Run("Create transporter", func(t *testing.T) { 30 | transporter = services.NewTransporter(&structures.Config{ 31 | PublicKey: "pubKey", 32 | PrivateKey: "privKey", 33 | Node: &defaultNode, // Normally set by pm2io 34 | }, "version") 35 | 36 | if transporter == nil { 37 | t.Fatal("transporter is nil") 38 | } 39 | }) 40 | 41 | t.Run("Mock wss", func(t *testing.T) { 42 | wssServer = httptest.NewServer(http.HandlerFunc(echo)) 43 | }) 44 | 45 | t.Run("Mock http server for verifier", func(t *testing.T) { 46 | verify := services.Verify{ 47 | PublicId: transporter.Config.PublicKey, 48 | PrivateId: transporter.Config.PrivateKey, 49 | Data: services.VerifyData{ 50 | MachineName: transporter.Config.Hostname, 51 | Cpus: runtime.NumCPU(), 52 | Memory: metrics.TotalMem(), 53 | Pm2Version: transporter.Version, 54 | Hostname: transporter.Config.ServerName, 55 | }, 56 | } 57 | 58 | gockVerif = gock.New("https://api.cloud.pm2.io"). 59 | Post("/api/node/verifyPM2"). 60 | MatchType("json"). 61 | JSON(verify). 62 | Reply(200). 63 | JSON(services.VerifyResponse{ 64 | Endpoints: services.Endpoints{ 65 | WS: "ws" + strings.TrimPrefix(wssServer.URL, "http"), 66 | }, 67 | }) 68 | }) 69 | 70 | t.Run("Connect transporter", func(t *testing.T) { 71 | transporter.Connect() 72 | if !transporter.IsConnected() { 73 | t.Fatal("transporter is not connected") 74 | } 75 | }) 76 | 77 | t.Run("Wait for ws connection", func(t *testing.T) { 78 | if nbConnected != 1 { 79 | t.Fatal("WS connected wanted: 1, connected: " + strconv.Itoa(nbConnected)) 80 | } 81 | }) 82 | 83 | t.Run("Try to close and reconnect", func(t *testing.T) { 84 | transporter.CloseAndReconnect() 85 | time.Sleep(2 * time.Second) 86 | if nbConnected != 1 { 87 | t.Fatal("WS connected wanted: 1, connected: " + strconv.Itoa(nbConnected)) 88 | } 89 | }) 90 | 91 | t.Run("Shouldn't crash without WSS", func(t *testing.T) { 92 | wssServer.Close() 93 | nbConnected = 0 94 | time.Sleep(2 * time.Second) 95 | }) 96 | 97 | t.Skip("Should get new node and connect to it") 98 | t.Run("Should get new node and connect to it", func(t *testing.T) { 99 | wssServer = httptest.NewServer(http.HandlerFunc(echo)) 100 | 101 | gockVerif.JSON(services.VerifyResponse{ 102 | Endpoints: services.Endpoints{ 103 | WS: "ws" + strings.TrimPrefix(wssServer.URL, "http"), 104 | }, 105 | }) 106 | 107 | newNode := "ws" + strings.TrimPrefix(wssServer.URL, "http") 108 | transporter.Config.Node = &newNode 109 | time.Sleep(8 * time.Second) 110 | if nbConnected != 1 { 111 | t.Log("Transporter node: " + *transporter.Config.Node) 112 | t.Log("Node wanted: " + "ws" + strings.TrimPrefix(wssServer.URL, "http")) 113 | t.Fatal("WS connected wanted: 1, connected: " + strconv.Itoa(nbConnected)) 114 | } 115 | }) 116 | } 117 | 118 | var upgrader = websocket.Upgrader{} 119 | 120 | func echo(w http.ResponseWriter, r *http.Request) { 121 | c, err := upgrader.Upgrade(w, r, nil) 122 | if err != nil { 123 | return 124 | } 125 | defer c.Close() 126 | nbConnected++ 127 | log.Println("New conn") 128 | for { 129 | _, _, err := c.ReadMessage() 130 | if err != nil { 131 | log.Println("Conn closed") 132 | nbConnected-- 133 | return 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/keymetrics/pm2-io-apm-go/services" 12 | 13 | "github.com/keymetrics/pm2-io-apm-go/structures" 14 | 15 | "github.com/keymetrics/pm2-io-apm-go" 16 | ) 17 | 18 | func main() { 19 | Pm2Io := pm2io.Pm2Io{ 20 | Config: &structures.Config{ 21 | PublicKey: "ft_public", 22 | PrivateKey: "ft_private", 23 | Name: "Golang App", 24 | }, 25 | } 26 | Pm2Io.Start() 27 | 28 | metric := structures.CreateMetric("test", "metric", "unit") 29 | services.AddMetric(&metric) 30 | 31 | nbreq := structures.Metric{ 32 | Name: "nbreq", 33 | Value: 0, 34 | } 35 | services.AddMetric(&nbreq) 36 | 37 | services.AddAction(&structures.Action{ 38 | ActionName: "Test", 39 | Callback: func(_ map[string]interface{}) string { 40 | log.Println("Action TEST") 41 | return "I am the test answer" 42 | }, 43 | }) 44 | 45 | services.AddAction(&structures.Action{ 46 | ActionName: "Tric", 47 | }) 48 | 49 | services.AddAction(&structures.Action{ 50 | ActionName: "Get env", 51 | Callback: func(_ map[string]interface{}) string { 52 | return strings.Join(os.Environ(), "\n") 53 | }, 54 | }) 55 | 56 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 57 | for i := 0; i < 1000; i++ { 58 | fmt.Fprintf(w, "Hello") 59 | } 60 | nbreq.Value++ 61 | }) 62 | 63 | go func() { 64 | ticker := time.NewTicker(2 * time.Second) 65 | log.Println("created 2s ticker") 66 | for { 67 | <-ticker.C 68 | metric.Value++ 69 | //cause := errors.New("Niaha") 70 | //err := errors.WithStack(cause) 71 | //Pm2Io.Notifier.Error(err) 72 | } 73 | }() 74 | 75 | go func() { 76 | ticker := time.NewTicker(4 * time.Second) 77 | log.Println("created log ticker") 78 | for { 79 | <-ticker.C 80 | Pm2Io.Notifier.Log("I love logging things\n") 81 | } 82 | }() 83 | 84 | /*go func() { 85 | ticker := time.NewTicker(10 * time.Second) 86 | log.Println("created reset ticker") 87 | for { 88 | <-ticker.C 89 | log.Println("RestartTransporter") 90 | Pm2Io.RestartTransporter() 91 | } 92 | }()*/ 93 | 94 | /*go func() { 95 | ticker := time.NewTicker(6 * time.Second) 96 | log.Println("created log ticker") 97 | for { 98 | <-ticker.C 99 | cause := errors.New("Fatal panic error") 100 | err := errors.WithStack(cause) 101 | Pm2Io.Panic(err) 102 | } 103 | }()*/ 104 | 105 | http.ListenAndServe(":8089", nil) 106 | } 107 | -------------------------------------------------------------------------------- /structures/action.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | // Action like AxmAction 4 | type Action struct { 5 | ActionName string `json:"action_name"` 6 | ActionType string `json:"action_type"` // default: "custom" else "internal" (like profiling, restart) 7 | Callback func(payload map[string]interface{}) string `json:"-"` 8 | } 9 | -------------------------------------------------------------------------------- /structures/config.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "os" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // Config from user code 12 | type Config struct { 13 | PublicKey string 14 | PrivateKey string 15 | Name string 16 | ServerName string 17 | Hostname string 18 | Node *string 19 | Proxy string 20 | ProcessUniqueID string 21 | } 22 | 23 | // InitNames with random values 24 | func (config *Config) InitNames() { 25 | realHostname, err := os.Hostname() 26 | if err != nil { 27 | config.Hostname = randomHex(5) 28 | } else { 29 | config.Hostname = realHostname 30 | } 31 | if config.ServerName == "" { 32 | config.ServerName = config.Hostname 33 | } 34 | config.ProcessUniqueID = uuid.Must(uuid.NewRandom()).String() 35 | } 36 | 37 | func randomHex(n int) string { 38 | bytes := make([]byte, n) 39 | if _, err := rand.Read(bytes); err != nil { 40 | return "" 41 | } 42 | return hex.EncodeToString(bytes) 43 | } 44 | -------------------------------------------------------------------------------- /structures/config_test.go: -------------------------------------------------------------------------------- 1 | package structures_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keymetrics/pm2-io-apm-go/structures" 7 | ) 8 | 9 | func TestConfig(t *testing.T) { 10 | var config structures.Config 11 | 12 | t.Run("Should create a config object", func(t *testing.T) { 13 | config = structures.Config{ 14 | Name: "golang_tests", 15 | } 16 | }) 17 | 18 | t.Run("Shouldn't have a serverName", func(t *testing.T) { 19 | if config.ServerName != "" { 20 | t.Fatal("Already have a serverName") 21 | } 22 | }) 23 | 24 | t.Run("Should set a serverName", func(t *testing.T) { 25 | config.InitNames() 26 | if config.ServerName == "" { 27 | t.Fatal("No serverName generated") 28 | } 29 | if config.Hostname == "" { 30 | t.Fatal("No hostname") 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /structures/metric.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | // Metric like AxmMonitor 4 | type Metric struct { 5 | Name string `json:"name"` 6 | Value float64 `json:"value"` 7 | Category string `json:"type"` 8 | Historic bool `json:"historic"` 9 | Unit string `json:"unit"` 10 | 11 | Aggregation string `json:"agg_type"` 12 | 13 | Function func() float64 `json:"-"` 14 | } 15 | 16 | // Get current value and check func if provided 17 | func (metric *Metric) Get() float64 { 18 | if metric.Function != nil { 19 | metric.Value = metric.Function() 20 | } 21 | return metric.Value 22 | } 23 | 24 | // Set current value 25 | func (metric *Metric) Set(value float64) { 26 | metric.Value = value 27 | } 28 | 29 | var defaultAggregation = "avg" 30 | 31 | // CreateMetric with default values 32 | func CreateMetric(name string, category string, unit string) Metric { 33 | return Metric{ 34 | Name: name, 35 | Category: category, 36 | Unit: unit, 37 | Value: 0, 38 | Aggregation: defaultAggregation, 39 | 40 | Historic: true, 41 | } 42 | } 43 | 44 | // CreateFuncMetric with default values and exposed function 45 | func CreateFuncMetric(name string, category string, unit string, cb func() float64) Metric { 46 | return Metric{ 47 | Name: name, 48 | Category: category, 49 | Unit: unit, 50 | Value: 0, 51 | Aggregation: defaultAggregation, 52 | 53 | Historic: true, 54 | Function: cb, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /structures/metric_test.go: -------------------------------------------------------------------------------- 1 | package structures_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keymetrics/pm2-io-apm-go/structures" 7 | ) 8 | 9 | func TestMetric(t *testing.T) { 10 | var funcMetric structures.Metric 11 | var stdMetric structures.Metric 12 | 13 | t.Run("create func metric", func(t *testing.T) { 14 | funcMetric = structures.CreateFuncMetric("name", "category", "unit", func() float64 { 15 | return 42 16 | }) 17 | }) 18 | 19 | t.Run("get func value", func(t *testing.T) { 20 | val := funcMetric.Get() 21 | if val != 42 { 22 | t.Fatal("bad func ret value") 23 | } 24 | }) 25 | 26 | t.Run("create std metric", func(t *testing.T) { 27 | stdMetric = structures.CreateMetric("name2", "category2", "unit2") 28 | }) 29 | 30 | t.Run("set value", func(t *testing.T) { 31 | stdMetric.Set(20000) 32 | }) 33 | 34 | t.Run("get value", func(t *testing.T) { 35 | val := stdMetric.Get() 36 | if val != 20000 { 37 | t.Fatal("bad std value") 38 | } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /structures/options.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | // Options like AxmOptions 4 | type Options struct { 5 | DefaultActions bool `json:"default_actions"` 6 | CustomProbes bool `json:"custom_probes"` 7 | Profiling bool `json:"profiling"` 8 | HeapDump bool `json:"heapdump"` 9 | Apm Apm `json:"apm"` 10 | } 11 | 12 | type Apm struct { 13 | Type string `json:"type"` 14 | Version string `json:"version"` 15 | } 16 | -------------------------------------------------------------------------------- /structures/process.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | // Process data 4 | type Process struct { 5 | Name string `json:"name"` 6 | Server string `json:"server"` 7 | Rev string `json:"rev"` 8 | PmID int `json:"pm_id"` 9 | } 10 | -------------------------------------------------------------------------------- /structures/profiling.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | import ( 4 | b64 "encoding/base64" 5 | "time" 6 | ) 7 | 8 | // ProfilingRequest from KM 9 | type ProfilingRequest struct { 10 | Timeout int64 `json:"timeout"` 11 | } 12 | 13 | // ProfilingResponse to KM (data as string) 14 | type ProfilingResponse struct { 15 | Data string `json:"data"` 16 | At int64 `json:"at"` 17 | Initiated string `json:"initiated"` 18 | Duration int `json:"duration"` 19 | Type string `json:"type"` 20 | Encoding string `json:"encoding"` 21 | } 22 | 23 | // NewProfilingResponse with default values 24 | func NewProfilingResponse(data []byte, element string) ProfilingResponse { 25 | res := ProfilingResponse{ 26 | Data: b64.StdEncoding.EncodeToString(data), 27 | At: time.Now().UnixNano() / int64(time.Millisecond), 28 | Initiated: "manual", 29 | Duration: 0, 30 | Type: element, 31 | Encoding: "base64", 32 | } 33 | return res 34 | } 35 | -------------------------------------------------------------------------------- /structures/server.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | // Server of status packet 4 | type Server struct { 5 | Loadavg []float64 `json:"loadavg"` 6 | TotalMem uint64 `json:"total_mem,omitempty"` 7 | FreeMem int64 `json:"free_mem,omitempty"` 8 | CPU CPU `json:"cpu"` 9 | Hostname string `json:"hostname"` 10 | Uptime int64 `json:"uptime"` 11 | Type string `json:"type"` 12 | Platform string `json:"platform"` 13 | Arch string `json:"arch"` 14 | User string `json:"user"` 15 | Interaction bool `json:"interaction"` 16 | Pm2Version string `json:"pm2_version"` 17 | NodeVersion string `json:"node_version"` 18 | RemoteIP string `json:"remote_ip"` 19 | RemotePort int `json:"remote_port"` 20 | } 21 | 22 | // CPU informations 23 | type CPU struct { 24 | Number int `json:"number"` 25 | Info string `json:"info"` 26 | } 27 | -------------------------------------------------------------------------------- /structures/status.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | // Status packet 4 | type Status struct { 5 | Process []StatusProcess `json:"process"` 6 | Server Server `json:"server"` 7 | } 8 | -------------------------------------------------------------------------------- /structures/statusprocess.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | // StatusProcess data 4 | type StatusProcess struct { 5 | Pid int32 `json:"pid"` 6 | Name string `json:"name"` 7 | Server string `json:"server"` 8 | Interpreter string `json:"interpreter,omitempty"` 9 | RestartTime int `json:"restart_time,omitempty"` 10 | CreatedAt int64 `json:"created_at,omitempty"` 11 | ExecMode string `json:"exec_mode,omitempty"` 12 | Watching bool `json:"watching,omitempty"` 13 | PmUptime int64 `json:"pm_uptime,omitempty"` 14 | Status string `json:"status,omitempty"` 15 | PmID int `json:"pm_id"` 16 | CPU float64 `json:"cpu"` 17 | Rev string `json:"rev"` 18 | Memory uint64 `json:"memory"` 19 | UniqueID string `json:"unique_id"` 20 | NodeEnv string `json:"node_env,omitempty"` 21 | AxmActions []*Action `json:"axm_actions,omitempty"` 22 | AxmMonitor map[string]*Metric `json:"axm_monitor,omitempty"` 23 | AxmOptions Options `json:"axm_options,omitempty"` 24 | } 25 | --------------------------------------------------------------------------------