├── .github └── workflows │ ├── test.yml │ └── wasm.yml ├── AUTHORS ├── LICENSE ├── README.md ├── callers.go ├── callers_test.go ├── cas_safe.go ├── cas_unsafe.go ├── collect ├── cas_safe.go ├── cas_unsafe.go ├── collect.go ├── ctx.go ├── doc.go └── trace.go ├── counter.go ├── ctx.go ├── ctx_test.go ├── dist.go ├── distgen.go.m4 ├── doc.go ├── durdist.go ├── environment ├── env.go ├── os.go ├── os_test.go ├── os_unix.go ├── os_windows.go ├── proc.go ├── proc_linux.go ├── proc_other.go ├── process.go ├── process_cc_darwin.go ├── process_cgo_darwin.go ├── process_test.go ├── process_unix.go ├── process_windows.go ├── runtime.go └── rusage.go ├── error_names.go ├── error_names_ae.go ├── error_names_net.go ├── error_names_net_tinygo.go ├── error_names_syscall.go ├── examples ├── minimal │ └── main.go └── wasm │ └── main.go ├── floatdist.go ├── func.go ├── func_test.go ├── funcset.go ├── funcstats.go ├── go.mod ├── go.sum ├── http ├── client.go ├── client_test.go ├── doc.go ├── http.go ├── http_test.go ├── info.go ├── info_test.go └── server.go ├── id.go ├── images ├── callgraph.png ├── callgraph2.png ├── handlehttp.png ├── logo.png ├── trace.png └── trace.svg ├── intdist.go ├── internal ├── testpkg1 │ └── main.go └── tests │ └── caller_test.go ├── meter.go ├── monotime ├── monotime.go ├── monotime_fallback.go ├── monotime_test.go └── monotime_windows.go ├── present ├── doc.go ├── dot.go ├── errs.go ├── funcs.go ├── http.go ├── json.go ├── path.go ├── spans.go ├── stats.go ├── trace.go └── utils.go ├── registry.go ├── rng.go ├── rng_test.go ├── scope.go ├── span.go ├── spanbag.go ├── spinlock.go ├── stats.go ├── struct.go ├── struct_test.go ├── tags.go ├── tags_test.go ├── task.go ├── timer.go ├── trace.go ├── transform.go └── val.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | go-version: ["1.21.x"] 16 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-go@v4 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | - run: go vet ./... 23 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yml: -------------------------------------------------------------------------------- 1 | name: wasm 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: "ubuntu-latest" 12 | strategy: 13 | fail-fast: false 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-go@v4 17 | with: 18 | go-version: "1.21.x" 19 | - uses: acifani/setup-tinygo@v1 20 | with: 21 | tinygo-version: "0.29.0" 22 | - run: GOOS=wasip1 GOARCH=wasm go vet ./... 23 | - run: GOOS=js GOARCH=wasm go vet ./... 24 | - run: GOOS=wasip1 GOARCH=wasm go build -o go.wasip.wasm ./examples/wasm 25 | - run: GOOS=js GOARCH=wasm go build -o go.js.wasm ./examples/wasm 26 | - run: tinygo build -target wasm -o tiny.wasm ./examples/wasm -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Space Monkey, Inc. 2 | -------------------------------------------------------------------------------- /callers.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "runtime" 19 | "strings" 20 | ) 21 | 22 | func callerPackage(frames int) string { 23 | var pc [1]uintptr 24 | if runtime.Callers(frames+2, pc[:]) != 1 { 25 | return "unknown" 26 | } 27 | frame, _ := runtime.CallersFrames(pc[:]).Next() 28 | if frame.Func == nil { 29 | return "unknown" 30 | } 31 | slash_pieces := strings.Split(frame.Func.Name(), "/") 32 | dot_pieces := strings.SplitN(slash_pieces[len(slash_pieces)-1], ".", 2) 33 | return strings.Join(slash_pieces[:len(slash_pieces)-1], "/") + "/" + dot_pieces[0] 34 | } 35 | 36 | func callerFunc(frames int) string { 37 | var pc [1]uintptr 38 | if runtime.Callers(frames+3, pc[:]) != 1 { 39 | return "unknown" 40 | } 41 | frame, _ := runtime.CallersFrames(pc[:]).Next() 42 | if frame.Function == "" { 43 | return "unknown" 44 | } 45 | funcname, ok := extractFuncName(frame.Function) 46 | if !ok { 47 | return "unknown" 48 | } 49 | return funcname 50 | } 51 | 52 | // extractFuncName splits fully qualified function name: 53 | // 54 | // Input: 55 | // 56 | // "github.com/spacemonkeygo/monkit/v3.BenchmarkTask.func1" 57 | // "main.DoThings.func1" 58 | // "main.DoThings" 59 | // 60 | // Output: 61 | // 62 | // funcname: "BenchmarkTask.func1" 63 | // funcname: "DoThings.func1" 64 | // funcname: "DoThings" 65 | func extractFuncName(fullyQualifiedName string) (funcname string, ok bool) { 66 | lastSlashPos := strings.LastIndexByte(fullyQualifiedName, '/') 67 | if lastSlashPos+1 >= len(fullyQualifiedName) { 68 | // fullyQualifiedName ended with slash. 69 | return "", false 70 | } 71 | 72 | qualifiedName := fullyQualifiedName[lastSlashPos+1:] 73 | packageDotPos := strings.IndexByte(qualifiedName, '.') 74 | if packageDotPos < 0 || packageDotPos+1 >= len(qualifiedName) { 75 | // qualifiedName ended with a dot 76 | return "", false 77 | } 78 | 79 | return qualifiedName[packageDotPos+1:], true 80 | } 81 | -------------------------------------------------------------------------------- /callers_test.go: -------------------------------------------------------------------------------- 1 | package monkit 2 | 3 | import "testing" 4 | 5 | func TestExtractFuncName(t *testing.T) { 6 | for _, test := range []struct { 7 | in string 8 | fn string 9 | ok bool 10 | }{ 11 | {"", "", false}, 12 | {"a/", "", false}, 13 | {"a/v.", "", false}, 14 | {"a/v.x", "x", true}, 15 | {"github.com/spacemonkeygo/monkit/v3.BenchmarkTask.func1", "BenchmarkTask.func1", true}, 16 | {"main.DoThings.func1", "DoThings.func1", true}, 17 | {"main.DoThings", "DoThings", true}, 18 | } { 19 | fn, ok := extractFuncName(test.in) 20 | if fn != test.fn || ok != test.ok { 21 | t.Errorf("failed %q, got %q %v, expected %q %v", test.in, fn, ok, test.fn, test.ok) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cas_safe.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build appengine 16 | // +build appengine 17 | 18 | package monkit 19 | 20 | import "sync" 21 | 22 | // TODO(jeff): make this mutex smaller scoped, perhaps based on the arguments 23 | // to compare and swap? 24 | var bigHonkinMutex sync.Mutex 25 | 26 | func loadFunc(addr **Func) (s *Func) { 27 | bigHonkinMutex.Lock() 28 | s = *addr 29 | bigHonkinMutex.Unlock() 30 | return s 31 | } 32 | 33 | func compareAndSwapFunc(addr **Func, old, new *Func) bool { 34 | bigHonkinMutex.Lock() 35 | val := *addr 36 | if val == old { 37 | *addr = new 38 | bigHonkinMutex.Unlock() 39 | return true 40 | } 41 | bigHonkinMutex.Unlock() 42 | return false 43 | } 44 | 45 | func loadTraceWatcherRef(addr **traceWatcherRef) (val *traceWatcherRef) { 46 | bigHonkinMutex.Lock() 47 | val = *addr 48 | bigHonkinMutex.Unlock() 49 | return val 50 | } 51 | 52 | func storeTraceWatcherRef(addr **traceWatcherRef, val *traceWatcherRef) { 53 | bigHonkinMutex.Lock() 54 | *addr = val 55 | bigHonkinMutex.Unlock() 56 | } 57 | 58 | func compareAndSwapSpanObserverTuple(addr **spanObserverTuple, 59 | old, new *spanObserverTuple) bool { 60 | bigHonkinMutex.Lock() 61 | val := *addr 62 | if val == old { 63 | *addr = new 64 | bigHonkinMutex.Unlock() 65 | return true 66 | } 67 | bigHonkinMutex.Unlock() 68 | return false 69 | } 70 | 71 | func loadSpanObserverTuple(addr **spanObserverTuple) (val *spanObserverTuple) { 72 | bigHonkinMutex.Lock() 73 | val = *addr 74 | bigHonkinMutex.Unlock() 75 | return val 76 | } 77 | -------------------------------------------------------------------------------- /cas_unsafe.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !appengine 16 | // +build !appengine 17 | 18 | package monkit 19 | 20 | import ( 21 | "sync/atomic" 22 | "unsafe" 23 | ) 24 | 25 | // 26 | // *Func atomic functions 27 | // 28 | 29 | func loadFunc(addr **Func) (val *Func) { 30 | return (*Func)(atomic.LoadPointer( 31 | (*unsafe.Pointer)(unsafe.Pointer(addr)))) 32 | } 33 | 34 | func compareAndSwapFunc(addr **Func, old, new *Func) bool { 35 | return atomic.CompareAndSwapPointer( 36 | (*unsafe.Pointer)(unsafe.Pointer(addr)), 37 | unsafe.Pointer(old), 38 | unsafe.Pointer(new)) 39 | } 40 | 41 | // 42 | // *traceWatcherRef atomic functions 43 | // 44 | 45 | func loadTraceWatcherRef(addr **traceWatcherRef) (val *traceWatcherRef) { 46 | return (*traceWatcherRef)(atomic.LoadPointer( 47 | (*unsafe.Pointer)(unsafe.Pointer(addr)))) 48 | } 49 | 50 | func storeTraceWatcherRef(addr **traceWatcherRef, val *traceWatcherRef) { 51 | atomic.StorePointer( 52 | (*unsafe.Pointer)(unsafe.Pointer(addr)), 53 | unsafe.Pointer(val)) 54 | } 55 | 56 | // 57 | // *spanObserverTuple atomic functons 58 | // 59 | 60 | func compareAndSwapSpanObserverTuple(addr **spanObserverTuple, 61 | old, new *spanObserverTuple) bool { 62 | return atomic.CompareAndSwapPointer( 63 | (*unsafe.Pointer)(unsafe.Pointer(addr)), 64 | unsafe.Pointer(old), 65 | unsafe.Pointer(new)) 66 | } 67 | 68 | func loadSpanObserverTuple(addr **spanObserverTuple) (val *spanObserverTuple) { 69 | return (*spanObserverTuple)(atomic.LoadPointer( 70 | (*unsafe.Pointer)(unsafe.Pointer(addr)))) 71 | } 72 | -------------------------------------------------------------------------------- /collect/cas_safe.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build appengine 16 | // +build appengine 17 | 18 | package collect 19 | 20 | import ( 21 | "sync" 22 | 23 | "github.com/spacemonkeygo/monkit/v3" 24 | ) 25 | 26 | // TODO(jeff): make this mutex smaller scoped, perhaps based on the arguments 27 | // to compare and swap? 28 | var bigHonkinMutex sync.Mutex 29 | 30 | func loadSpan(addr **monkit.Span) (s *monkit.Span) { 31 | bigHonkinMutex.Lock() 32 | s = *addr 33 | bigHonkinMutex.Unlock() 34 | return s 35 | } 36 | 37 | func swapSpan(addr **monkit.Span, new *monkit.Span) *monkit.Span { 38 | bigHonkinMutex.Lock() 39 | prev := *addr 40 | *addr = new 41 | bigHonkinMutex.Unlock() 42 | return prev 43 | } 44 | 45 | func compareAndSwapSpan(addr **monkit.Span, old, new *monkit.Span) bool { 46 | bigHonkinMutex.Lock() 47 | val := *addr 48 | if val == old { 49 | *addr = new 50 | bigHonkinMutex.Unlock() 51 | return true 52 | } 53 | bigHonkinMutex.Unlock() 54 | return false 55 | } 56 | -------------------------------------------------------------------------------- /collect/cas_unsafe.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !appengine 16 | // +build !appengine 17 | 18 | package collect 19 | 20 | import ( 21 | "sync/atomic" 22 | "unsafe" 23 | 24 | "github.com/spacemonkeygo/monkit/v3" 25 | ) 26 | 27 | func loadSpan(addr **monkit.Span) (s *monkit.Span) { 28 | return (*monkit.Span)(atomic.LoadPointer( 29 | (*unsafe.Pointer)(unsafe.Pointer(addr)))) 30 | } 31 | 32 | func swapSpan(addr **monkit.Span, new *monkit.Span) *monkit.Span { 33 | return (*monkit.Span)(atomic.SwapPointer( 34 | (*unsafe.Pointer)(unsafe.Pointer(addr)), 35 | unsafe.Pointer(new))) 36 | } 37 | 38 | func compareAndSwapSpan(addr **monkit.Span, old, new *monkit.Span) bool { 39 | return atomic.CompareAndSwapPointer( 40 | (*unsafe.Pointer)(unsafe.Pointer(addr)), 41 | unsafe.Pointer(old), 42 | unsafe.Pointer(new)) 43 | } 44 | -------------------------------------------------------------------------------- /collect/collect.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package collect 16 | 17 | import ( 18 | "sort" 19 | "sync" 20 | "sync/atomic" 21 | "time" 22 | 23 | "github.com/spacemonkeygo/monkit/v3" 24 | ) 25 | 26 | // FinishedSpan is a Span that has completed and contains information about 27 | // how it finished. 28 | type FinishedSpan struct { 29 | Span *monkit.Span 30 | Err error 31 | Panicked bool 32 | Finish time.Time 33 | } 34 | 35 | type spanParent struct { 36 | parentId int64 37 | ok bool 38 | } 39 | 40 | // SpanCollector implements the SpanObserver interface. It stores all Spans 41 | // observed after it starts collecting, typically when matcher returns true. 42 | type SpanCollector struct { 43 | // sync/atomic 44 | check *monkit.Span 45 | 46 | // construction 47 | matcher func(s *monkit.Span) bool 48 | done chan struct{} 49 | 50 | // mtx protected 51 | mtx sync.Mutex 52 | root *FinishedSpan 53 | spansByParent map[spanParent][]*FinishedSpan 54 | } 55 | 56 | // NewSpanCollector takes a matcher that will return true when a span is found 57 | // that should start collection. matcher can be nil if you intend to use 58 | // ForceStart instead. 59 | func NewSpanCollector(matcher func(s *monkit.Span) bool) ( 60 | rv *SpanCollector) { 61 | if matcher == nil { 62 | matcher = func(*monkit.Span) bool { return false } 63 | } 64 | return &SpanCollector{ 65 | matcher: matcher, 66 | done: make(chan struct{}), 67 | spansByParent: map[spanParent][]*FinishedSpan{}, 68 | } 69 | } 70 | 71 | // Done returns a channel that's closed when the SpanCollector has collected 72 | // everything it cares about. 73 | func (c *SpanCollector) Done() <-chan struct{} { 74 | return c.done 75 | } 76 | 77 | var donePointer = new(monkit.Span) 78 | 79 | // ForceStart starts the span collector collecting spans, stopping when endSpan 80 | // finishes. This is typically only used if matcher was nil at construction. 81 | func (c *SpanCollector) ForceStart(endSpan *monkit.Span) { 82 | compareAndSwapSpan(&c.check, nil, endSpan) 83 | } 84 | 85 | // Start is to implement the monkit.SpanObserver interface. Start gets called 86 | // whenever a Span starts. 87 | func (c *SpanCollector) Start(s *monkit.Span) { 88 | if loadSpan(&c.check) != nil || !c.matcher(s) { 89 | return 90 | } 91 | compareAndSwapSpan(&c.check, nil, s) 92 | } 93 | 94 | // Finish is to implement the monkit.SpanObserver interface. Finish gets 95 | // called whenever a Span finishes. 96 | func (c *SpanCollector) Finish(s *monkit.Span, err error, panicked bool, 97 | finish time.Time) { 98 | existing := loadSpan(&c.check) 99 | if existing == donePointer || existing == nil || 100 | existing.Trace() != s.Trace() { 101 | return 102 | } 103 | fs := &FinishedSpan{Span: s, Err: err, Panicked: panicked, Finish: finish} 104 | c.mtx.Lock() 105 | if c.root != nil { 106 | c.mtx.Unlock() 107 | return 108 | } 109 | if existing == s { 110 | c.root = fs 111 | c.mtx.Unlock() 112 | c.Stop() 113 | } else { 114 | id, ok := s.ParentId() 115 | key := spanParent{id, ok} 116 | c.spansByParent[key] = append(c.spansByParent[key], fs) 117 | c.mtx.Unlock() 118 | } 119 | } 120 | 121 | // Stop stops the SpanCollector from collecting. 122 | func (c *SpanCollector) Stop() { 123 | if swapSpan(&c.check, donePointer) != donePointer { 124 | close(c.done) 125 | } 126 | } 127 | 128 | // Spans returns all spans found, rooted from the Span the collector started 129 | // on. 130 | func (c *SpanCollector) Spans() (spans []*FinishedSpan) { 131 | var walkSpans func(s *FinishedSpan) 132 | walkSpans = func(s *FinishedSpan) { 133 | spans = append(spans, s) 134 | for _, child := range c.spansByParent[spanParent{s.Span.Id(), true}] { 135 | walkSpans(child) 136 | } 137 | } 138 | c.mtx.Lock() 139 | if c.root != nil { 140 | walkSpans(c.root) 141 | } 142 | c.mtx.Unlock() 143 | return spans 144 | } 145 | 146 | // StartTimeSorter assists with sorting a slice of FinishedSpans by start time. 147 | type StartTimeSorter []*FinishedSpan 148 | 149 | func (s StartTimeSorter) Len() int { return len(s) } 150 | func (s StartTimeSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 151 | 152 | func (s StartTimeSorter) Less(i, j int) bool { 153 | return s[i].Span.Start().UnixNano() < s[j].Span.Start().UnixNano() 154 | } 155 | 156 | func (s StartTimeSorter) Sort() { sort.Sort(s) } 157 | 158 | type spanFinder struct { 159 | doneFlag int32 160 | doneCh chan struct{} 161 | matcher func(s *monkit.Span) bool 162 | } 163 | 164 | func newSpanFinder(matcher func(s *monkit.Span) bool) *spanFinder { 165 | return &spanFinder{ 166 | doneCh: make(chan struct{}), 167 | matcher: matcher, 168 | } 169 | } 170 | 171 | func (m *spanFinder) Start(s *monkit.Span) { 172 | if atomic.LoadInt32(&m.doneFlag) != 0 { 173 | return 174 | } 175 | if m.matcher(s) { 176 | m.Stop() 177 | } 178 | } 179 | 180 | func (m *spanFinder) Finish(s *monkit.Span, err error, panicked bool, 181 | finish time.Time) { 182 | } 183 | 184 | func (m *spanFinder) Stop() { 185 | if atomic.SwapInt32(&m.doneFlag, 1) == 0 { 186 | close(m.doneCh) 187 | } 188 | } 189 | 190 | func (m *spanFinder) Done() <-chan struct{} { 191 | return m.doneCh 192 | } 193 | -------------------------------------------------------------------------------- /collect/ctx.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package collect 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/spacemonkeygo/monkit/v3" 22 | ) 23 | 24 | // WatchForSpans will watch for spans that 'matcher' returns true for. As soon 25 | // as a trace generates a matched span, all spans from that trace that finish 26 | // from that point on are collected until the matching span completes. All 27 | // collected spans are returned. 28 | // To cancel this operation, simply cancel the ctx argument. 29 | // There is a small but permanent amount of overhead added by this function to 30 | // every trace that is started while this function is running. This only really 31 | // affects long-running traces. 32 | func WatchForSpans(ctx context.Context, r *monkit.Registry, 33 | matcher func(s *monkit.Span) bool) (spans []*FinishedSpan, err error) { 34 | collector := NewSpanCollector(matcher) 35 | defer collector.Stop() 36 | 37 | cancel := ObserveAllTraces(r, collector) 38 | defer cancel() 39 | 40 | select { 41 | case <-ctx.Done(): 42 | return nil, ctx.Err() 43 | case <-collector.Done(): 44 | return collector.Spans(), nil 45 | } 46 | } 47 | 48 | // CollectSpans is kind of like WatchForSpans, except that it uses the current 49 | // span to figure out which trace to collect. It calls work(), then collects 50 | // from the current trace until work() returns. CollectSpans won't work unless 51 | // some ancestor function is also monitored and has modified the ctx. 52 | func CollectSpans(ctx context.Context, work func(ctx context.Context)) ( 53 | spans []*FinishedSpan) { 54 | s := monkit.SpanFromCtx(ctx) 55 | if s == nil { 56 | work(ctx) 57 | return nil 58 | } 59 | collector := NewSpanCollector(nil) 60 | defer collector.Stop() 61 | s.Trace().ObserveSpans(collector) 62 | f := s.Func() 63 | newF := f.Scope().FuncNamed(fmt.Sprintf("%s-TRACED", f.ShortName())) 64 | func() { 65 | defer newF.Task(&ctx)(nil) 66 | collector.ForceStart(monkit.SpanFromCtx(ctx)) 67 | work(ctx) 68 | }() 69 | return collector.Spans() 70 | } 71 | 72 | // FindSpan will call matcher until matcher returns true. Due to 73 | // the nature of span creation, matcher is likely to be concurrently 74 | // called and therefore matcher may get more than one matching span. 75 | func FindSpan(ctx context.Context, r *monkit.Registry, 76 | matcher func(s *monkit.Span) bool) { 77 | if matcher == nil { 78 | return 79 | } 80 | 81 | collector := newSpanFinder(matcher) 82 | defer collector.Stop() 83 | 84 | defer ObserveAllTraces(r, collector)() 85 | 86 | select { 87 | case <-ctx.Done(): 88 | case <-collector.Done(): 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /collect/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package collect // import "github.com/spacemonkeygo/monkit/v3/collect" 16 | -------------------------------------------------------------------------------- /collect/trace.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Storj Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package collect 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/spacemonkeygo/monkit/v3" 21 | ) 22 | 23 | // ObserveAllTraces will register collector with all traces present and future 24 | // on the given monkit.Registry until cancel is called. 25 | func ObserveAllTraces(r *monkit.Registry, collector monkit.SpanObserver) (cancel func()) { 26 | var mtx sync.Mutex 27 | var cancelers []func() 28 | var stopping bool 29 | existingTraces := map[*monkit.Trace]bool{} 30 | 31 | mainCanceler := r.ObserveTraces(func(t *monkit.Trace) { 32 | mtx.Lock() 33 | defer mtx.Unlock() 34 | if existingTraces[t] || stopping { 35 | return 36 | } 37 | existingTraces[t] = true 38 | cancelers = append(cancelers, t.ObserveSpans(collector)) 39 | }) 40 | 41 | // pick up live traces we can find 42 | r.RootSpans(func(s *monkit.Span) { 43 | mtx.Lock() 44 | defer mtx.Unlock() 45 | t := s.Trace() 46 | if existingTraces[t] || stopping { 47 | return 48 | } 49 | existingTraces[t] = true 50 | cancelers = append(cancelers, t.ObserveSpans(collector)) 51 | }) 52 | 53 | return func() { 54 | mainCanceler() 55 | mtx.Lock() 56 | defer mtx.Unlock() 57 | stopping = true 58 | for _, canceler := range cancelers { 59 | canceler() 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /counter.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "math" 19 | "sync" 20 | ) 21 | 22 | // Counter keeps track of running totals, along with the highest and lowest 23 | // values seen. The overall value can increment or decrement. Counter 24 | // implements StatSource. Should be constructed with NewCounter(), though it 25 | // may be more convenient to use the Counter accessor on a given Scope. 26 | // Expected creation is like: 27 | // 28 | // var mon = monkit.Package() 29 | // 30 | // func MyFunc() { 31 | // mon.Counter("beans").Inc(1) 32 | // } 33 | type Counter struct { 34 | mtx sync.Mutex 35 | val, low, high int64 36 | nonempty bool 37 | key SeriesKey 38 | } 39 | 40 | // NewCounter constructs a counter 41 | func NewCounter(key SeriesKey) *Counter { 42 | return &Counter{key: key} 43 | } 44 | 45 | func (c *Counter) set(val int64) { 46 | c.val = val 47 | if !c.nonempty || val < c.low { 48 | c.low = val 49 | } 50 | if !c.nonempty || c.high < val { 51 | c.high = val 52 | } 53 | c.nonempty = true 54 | } 55 | 56 | // Set will immediately change the value of the counter to whatever val is. It 57 | // will appropriately update the high and low values, and return the former 58 | // value. 59 | func (c *Counter) Set(val int64) (former int64) { 60 | c.mtx.Lock() 61 | former = c.val 62 | c.set(val) 63 | c.mtx.Unlock() 64 | return former 65 | } 66 | 67 | // Inc will atomically increment the counter by delta and return the new value. 68 | func (c *Counter) Inc(delta int64) (current int64) { 69 | c.mtx.Lock() 70 | c.set(c.val + delta) 71 | current = c.val 72 | c.mtx.Unlock() 73 | return current 74 | } 75 | 76 | // Dec will atomically decrement the counter by delta and return the new value. 77 | func (c *Counter) Dec(delta int64) (current int64) { 78 | return c.Inc(-delta) 79 | } 80 | 81 | // High returns the highest value seen since construction or the last reset 82 | func (c *Counter) High() (h int64) { 83 | c.mtx.Lock() 84 | h = c.high 85 | c.mtx.Unlock() 86 | return h 87 | } 88 | 89 | // Low returns the lowest value seen since construction or the last reset 90 | func (c *Counter) Low() (l int64) { 91 | c.mtx.Lock() 92 | l = c.low 93 | c.mtx.Unlock() 94 | return l 95 | } 96 | 97 | // Current returns the current value 98 | func (c *Counter) Current() (cur int64) { 99 | c.mtx.Lock() 100 | cur = c.val 101 | c.mtx.Unlock() 102 | return cur 103 | } 104 | 105 | // Reset resets all values including high/low counters and returns what they 106 | // were. 107 | func (c *Counter) Reset() (val, low, high int64) { 108 | c.mtx.Lock() 109 | val, low, high = c.val, c.low, c.high 110 | c.val, c.low, c.high, c.nonempty = 0, 0, 0, false 111 | c.mtx.Unlock() 112 | return val, low, high 113 | } 114 | 115 | // Stats implements the StatSource interface 116 | func (c *Counter) Stats(cb func(key SeriesKey, field string, val float64)) { 117 | c.mtx.Lock() 118 | val, low, high, nonempty := c.val, c.low, c.high, c.nonempty 119 | c.mtx.Unlock() 120 | if nonempty { 121 | cb(c.key, "high", float64(high)) 122 | cb(c.key, "low", float64(low)) 123 | } else { 124 | cb(c.key, "high", math.NaN()) 125 | cb(c.key, "low", math.NaN()) 126 | } 127 | cb(c.key, "value", float64(val)) 128 | } 129 | -------------------------------------------------------------------------------- /ctx_test.go: -------------------------------------------------------------------------------- 1 | package monkit 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TestLateObserver checks that if you add an observer to a trace after it has 10 | // begun, the original span that started the trace will also be observed 11 | func TestLateObserver(t *testing.T) { 12 | ctx := context.Background() 13 | mon := Package() 14 | mock := &mockSpanObserver{} 15 | func() { 16 | // Start the first span, this is the trace 17 | defer mon.Task()(&ctx)(nil) 18 | 19 | // Start a second span, this is a span on the trace, but still occurs 20 | // before we register a span observer 21 | defer mon.Task()(&ctx)(nil) 22 | 23 | // Now start observing all new traces 24 | mon.r.ObserveTraces(func(t *Trace) { 25 | t.ObserveSpans(mock) 26 | }) 27 | 28 | // Now go through all root spans, and observe the traces associated 29 | // with those spans 30 | mon.r.RootSpans(func(s *Span) { 31 | s.Trace().ObserveSpans(mock) 32 | }) 33 | 34 | // One more after-the-fact 35 | defer mon.Task()(&ctx)(nil) 36 | 37 | // Here is a new trace, to prove that is working 38 | ctx2 := context.Background() 39 | defer mon.Task()(&ctx2)(nil) 40 | }() 41 | expStarts := 2 42 | expFinishes := 4 43 | if mock.starts != expStarts { 44 | t.Errorf("Expected %d, got %d", expStarts, mock.starts) 45 | } 46 | if mock.finishes != expFinishes { 47 | t.Errorf("Expected %d, got %d", expFinishes, mock.finishes) 48 | } 49 | } 50 | 51 | type mockSpanObserver struct { 52 | starts, finishes int 53 | } 54 | 55 | func (m *mockSpanObserver) Start(s *Span) { 56 | m.starts++ 57 | } 58 | 59 | func (m *mockSpanObserver) Finish(s *Span, err error, panicked bool, finish time.Time) { 60 | m.finishes++ 61 | } 62 | 63 | func BenchmarkTask(b *testing.B) { 64 | mon := Package() 65 | pctx := context.Background() 66 | for i := 0; i < b.N; i++ { 67 | var err error 68 | func() { 69 | ctx := pctx 70 | defer mon.Task()(&ctx)(&err) 71 | }() 72 | } 73 | } 74 | func BenchmarkTaskNested(b *testing.B) { 75 | mon := Package() 76 | pctx := context.Background() 77 | var errout error 78 | defer mon.Task()(&pctx)(&errout) 79 | for i := 0; i < b.N; i++ { 80 | var err error 81 | func() { 82 | ctx := pctx 83 | defer mon.Task()(&ctx)(&err) 84 | }() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /dist.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | const ( 22 | ReservoirSize = 64 23 | ) 24 | 25 | var ( 26 | // If Window is > 0, the probability of replacing a datapoint will never 27 | // fall below ReservoirSize/Window instead of continuing to fall over time. 28 | // Window should be a multiple of ReservoirSize. 29 | Window int64 = 1024 30 | ) 31 | 32 | // ObservedQuantiles is the set of quantiles the internal distribution 33 | // measurement logic will try to optimize for, if applicable. 34 | var ObservedQuantiles = []float64{0, .1, .25, .5, .75, .9, .95, 1} 35 | 36 | type float32Slice []float32 37 | 38 | func (p float32Slice) Len() int { return len(p) } 39 | func (p float32Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 40 | func (p float32Slice) Less(i, j int) bool { 41 | // N.B.: usually, float comparisons should check if either value is NaN, but 42 | // in this package's usage, they never are here. 43 | return p[i] < p[j] 44 | } 45 | 46 | //go:generate sh -c "m4 -D_IMPORT_='\"time\"' -D_NAME_=Duration -D_LOWER_NAME_=duration -D_TYPE_=time.Duration distgen.go.m4 > durdist.go" 47 | //go:generate sh -c "m4 -D_IMPORT_= -D_NAME_=Float -D_LOWER_NAME_=float -D_TYPE_=float64 distgen.go.m4 > floatdist.go" 48 | //go:generate sh -c "m4 -D_IMPORT_= -D_NAME_=Int -D_LOWER_NAME_=int -D_TYPE_=int64 distgen.go.m4 > intdist.go" 49 | //go:generate gofmt -w -s durdist.go floatdist.go intdist.go 50 | 51 | func (d *DurationDist) toFloat64(v time.Duration) float64 { 52 | return v.Seconds() 53 | } 54 | 55 | func (d *IntDist) toFloat64(v int64) float64 { 56 | return float64(v) 57 | } 58 | 59 | func (d *FloatDist) toFloat64(v float64) float64 { 60 | return v 61 | } 62 | -------------------------------------------------------------------------------- /distgen.go.m4: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // WARNING: THE NON-M4 VERSIONS OF THIS FILE ARE GENERATED BY GO GENERATE! 16 | // ONLY MAKE CHANGES TO THE M4 FILE 17 | // 18 | 19 | package monkit 20 | 21 | import ( 22 | "sort" 23 | _IMPORT_ 24 | ) 25 | 26 | // _NAME_`Dist' keeps statistics about values such as 27 | // low/high/recent/average/quantiles. Not threadsafe. Construct with 28 | // `New'_NAME_`Dist'(). Fields are expected to be read from but not written to. 29 | type _NAME_`Dist' struct { 30 | // Low and High are the lowest and highest values observed since 31 | // construction or the last reset. 32 | Low, High _TYPE_ 33 | 34 | // Recent is the last observed value. 35 | Recent _TYPE_ 36 | 37 | // Count is the number of observed values since construction or the last 38 | // reset. 39 | Count int64 40 | 41 | // Sum is the sum of all the observed values since construction or the last 42 | // reset. 43 | Sum _TYPE_ 44 | 45 | key SeriesKey 46 | reservoir [ReservoirSize]float32 47 | rng xorshift128 48 | sorted bool 49 | } 50 | 51 | func `init'_NAME_`Dist'(v *_NAME_`Dist', key SeriesKey) { 52 | v.key = key 53 | v.rng = newXORShift128() 54 | } 55 | 56 | // `New'_NAME_`Dist' creates a distribution of _TYPE_`s'. 57 | func `New'_NAME_`Dist'(key SeriesKey) (d *_NAME_`Dist') { 58 | d = &_NAME_`Dist'{} 59 | `init'_NAME_`Dist'(d, key) 60 | return d 61 | } 62 | 63 | // Insert adds a value to the distribution, updating appropriate values. 64 | func (d *_NAME_`Dist') Insert(val _TYPE_) { 65 | if d.Count != 0 { 66 | if val < d.Low { 67 | d.Low = val 68 | } 69 | if val > d.High { 70 | d.High = val 71 | } 72 | } else { 73 | d.Low = val 74 | d.High = val 75 | } 76 | d.Recent = val 77 | d.Sum += val 78 | 79 | index := d.Count 80 | d.Count += 1 81 | 82 | if index < ReservoirSize { 83 | d.reservoir[index] = float32(val) 84 | d.sorted = false 85 | } else { 86 | window := d.Count 87 | // careful, the capitalization of Window is important 88 | if Window > 0 && window > Window { 89 | window = Window 90 | } 91 | // fast, but kind of biased. probably okay 92 | j := d.rng.Uint64() % uint64(window) 93 | if j < ReservoirSize { 94 | d.reservoir[int(j)] = float32(val) 95 | d.sorted = false 96 | } 97 | } 98 | } 99 | 100 | // FullAverage calculates and returns the average of all inserted values. 101 | func (d *_NAME_`Dist') FullAverage() _TYPE_ { 102 | if d.Count > 0 { 103 | return d.Sum / _TYPE_`(d.Count)' 104 | } 105 | return 0 106 | } 107 | 108 | // ReservoirAverage calculates the average of the current reservoir. 109 | func (d *_NAME_`Dist') ReservoirAverage() _TYPE_ { 110 | amount := ReservoirSize 111 | if d.Count < int64(amount) { 112 | amount = int(d.Count) 113 | } 114 | if amount <= 0 { 115 | return 0 116 | } 117 | var sum float32 118 | for i := 0; i < amount; i++ { 119 | sum += d.reservoir[i] 120 | } 121 | return _TYPE_`(sum / float32(amount))' 122 | } 123 | 124 | // Query will return the approximate value at the given quantile from the 125 | // reservoir, where 0 <= quantile <= 1. 126 | func (d *_NAME_`Dist') Query(quantile float64) _TYPE_ { 127 | rlen := int(ReservoirSize) 128 | if int64(rlen) > d.Count { 129 | rlen = int(d.Count) 130 | } 131 | 132 | if rlen < 2 { 133 | return _TYPE_`(d.reservoir[0])' 134 | } 135 | 136 | reservoir := d.reservoir[:rlen] 137 | if !d.sorted { 138 | sort.Sort(float32Slice(reservoir)) 139 | d.sorted = true 140 | } 141 | 142 | if quantile <= 0 { 143 | return _TYPE_`(reservoir[0])' 144 | } 145 | if quantile >= 1 { 146 | return _TYPE_`(reservoir[rlen-1])' 147 | } 148 | 149 | idx_float := quantile * float64(rlen-1) 150 | idx := int(idx_float) 151 | 152 | diff := idx_float - float64(idx) 153 | prior := float64(reservoir[idx]) 154 | return _TYPE_`(prior + diff*(float64(reservoir[idx+1])-prior))' 155 | } 156 | 157 | // Copy returns a full copy of the entire distribution. 158 | func (d *_NAME_`Dist') Copy() *_NAME_`Dist' { 159 | cp := *d 160 | cp.rng = newXORShift128() 161 | return &cp 162 | } 163 | 164 | func (d *_NAME_`Dist') Reset() { 165 | d.Low, d.High, d.Recent, d.Count, d.Sum = 0, 0, 0, 0, 0 166 | // resetting count will reset the quantile reservoir 167 | } 168 | 169 | func (d *_NAME_`Dist') Stats(cb func(key SeriesKey, field string, val float64)) { 170 | count := d.Count 171 | cb(d.key, "count", float64(count)) 172 | if count > 0 { 173 | cb(d.key, "sum", d.toFloat64(d.Sum)) 174 | cb(d.key, "min", d.toFloat64(d.Low)) 175 | cb(d.key, "max", d.toFloat64(d.High)) 176 | cb(d.key, "rmin", d.toFloat64(d.Query(0))) 177 | cb(d.key, "ravg", d.toFloat64(d.ReservoirAverage())) 178 | cb(d.key, "r10", d.toFloat64(d.Query(.1))) 179 | cb(d.key, "r50", d.toFloat64(d.Query(.5))) 180 | cb(d.key, "r90", d.toFloat64(d.Query(.9))) 181 | cb(d.key, "r99", d.toFloat64(d.Query(.99))) 182 | cb(d.key, "rmax", d.toFloat64(d.Query(1))) 183 | cb(d.key, "recent", d.toFloat64(d.Recent)) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /durdist.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // WARNING: THE NON-M4 VERSIONS OF THIS FILE ARE GENERATED BY GO GENERATE! 16 | // ONLY MAKE CHANGES TO THE M4 FILE 17 | // 18 | 19 | package monkit 20 | 21 | import ( 22 | "sort" 23 | "time" 24 | ) 25 | 26 | // DurationDist keeps statistics about values such as 27 | // low/high/recent/average/quantiles. Not threadsafe. Construct with 28 | // NewDurationDist(). Fields are expected to be read from but not written to. 29 | type DurationDist struct { 30 | // Low and High are the lowest and highest values observed since 31 | // construction or the last reset. 32 | Low, High time.Duration 33 | 34 | // Recent is the last observed value. 35 | Recent time.Duration 36 | 37 | // Count is the number of observed values since construction or the last 38 | // reset. 39 | Count int64 40 | 41 | // Sum is the sum of all the observed values since construction or the last 42 | // reset. 43 | Sum time.Duration 44 | 45 | key SeriesKey 46 | reservoir [ReservoirSize]float32 47 | rng xorshift128 48 | sorted bool 49 | } 50 | 51 | func initDurationDist(v *DurationDist, key SeriesKey) { 52 | v.key = key 53 | v.rng = newXORShift128() 54 | } 55 | 56 | // NewDurationDist creates a distribution of time.Durations. 57 | func NewDurationDist(key SeriesKey) (d *DurationDist) { 58 | d = &DurationDist{} 59 | initDurationDist(d, key) 60 | return d 61 | } 62 | 63 | // Insert adds a value to the distribution, updating appropriate values. 64 | func (d *DurationDist) Insert(val time.Duration) { 65 | if d.Count != 0 { 66 | if val < d.Low { 67 | d.Low = val 68 | } 69 | if val > d.High { 70 | d.High = val 71 | } 72 | } else { 73 | d.Low = val 74 | d.High = val 75 | } 76 | d.Recent = val 77 | d.Sum += val 78 | 79 | index := d.Count 80 | d.Count += 1 81 | 82 | if index < ReservoirSize { 83 | d.reservoir[index] = float32(val) 84 | d.sorted = false 85 | } else { 86 | window := d.Count 87 | // careful, the capitalization of Window is important 88 | if Window > 0 && window > Window { 89 | window = Window 90 | } 91 | // fast, but kind of biased. probably okay 92 | j := d.rng.Uint64() % uint64(window) 93 | if j < ReservoirSize { 94 | d.reservoir[int(j)] = float32(val) 95 | d.sorted = false 96 | } 97 | } 98 | } 99 | 100 | // FullAverage calculates and returns the average of all inserted values. 101 | func (d *DurationDist) FullAverage() time.Duration { 102 | if d.Count > 0 { 103 | return d.Sum / time.Duration(d.Count) 104 | } 105 | return 0 106 | } 107 | 108 | // ReservoirAverage calculates the average of the current reservoir. 109 | func (d *DurationDist) ReservoirAverage() time.Duration { 110 | amount := ReservoirSize 111 | if d.Count < int64(amount) { 112 | amount = int(d.Count) 113 | } 114 | if amount <= 0 { 115 | return 0 116 | } 117 | var sum float32 118 | for i := 0; i < amount; i++ { 119 | sum += d.reservoir[i] 120 | } 121 | return time.Duration(sum / float32(amount)) 122 | } 123 | 124 | // Query will return the approximate value at the given quantile from the 125 | // reservoir, where 0 <= quantile <= 1. 126 | func (d *DurationDist) Query(quantile float64) time.Duration { 127 | rlen := int(ReservoirSize) 128 | if int64(rlen) > d.Count { 129 | rlen = int(d.Count) 130 | } 131 | 132 | if rlen < 2 { 133 | return time.Duration(d.reservoir[0]) 134 | } 135 | 136 | reservoir := d.reservoir[:rlen] 137 | if !d.sorted { 138 | sort.Sort(float32Slice(reservoir)) 139 | d.sorted = true 140 | } 141 | 142 | if quantile <= 0 { 143 | return time.Duration(reservoir[0]) 144 | } 145 | if quantile >= 1 { 146 | return time.Duration(reservoir[rlen-1]) 147 | } 148 | 149 | idx_float := quantile * float64(rlen-1) 150 | idx := int(idx_float) 151 | 152 | diff := idx_float - float64(idx) 153 | prior := float64(reservoir[idx]) 154 | return time.Duration(prior + diff*(float64(reservoir[idx+1])-prior)) 155 | } 156 | 157 | // Copy returns a full copy of the entire distribution. 158 | func (d *DurationDist) Copy() *DurationDist { 159 | cp := *d 160 | cp.rng = newXORShift128() 161 | return &cp 162 | } 163 | 164 | func (d *DurationDist) Reset() { 165 | d.Low, d.High, d.Recent, d.Count, d.Sum = 0, 0, 0, 0, 0 166 | // resetting count will reset the quantile reservoir 167 | } 168 | 169 | func (d *DurationDist) Stats(cb func(key SeriesKey, field string, val float64)) { 170 | count := d.Count 171 | cb(d.key, "count", float64(count)) 172 | if count > 0 { 173 | cb(d.key, "sum", d.toFloat64(d.Sum)) 174 | cb(d.key, "min", d.toFloat64(d.Low)) 175 | cb(d.key, "max", d.toFloat64(d.High)) 176 | cb(d.key, "rmin", d.toFloat64(d.Query(0))) 177 | cb(d.key, "ravg", d.toFloat64(d.ReservoirAverage())) 178 | cb(d.key, "r10", d.toFloat64(d.Query(.1))) 179 | cb(d.key, "r50", d.toFloat64(d.Query(.5))) 180 | cb(d.key, "r90", d.toFloat64(d.Query(.9))) 181 | cb(d.key, "r99", d.toFloat64(d.Query(.99))) 182 | cb(d.key, "rmax", d.toFloat64(d.Query(1))) 183 | cb(d.key, "recent", d.toFloat64(d.Recent)) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /environment/env.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environment // import "github.com/spacemonkeygo/monkit/v3/environment" 16 | 17 | import ( 18 | "github.com/spacemonkeygo/monkit/v3" 19 | ) 20 | 21 | var ( 22 | registrations = []monkit.StatSource{} 23 | ) 24 | 25 | // Register attaches all of this package's environment data to the given 26 | // registry. It will be attached to a top-level scope called 'env'. 27 | func Register(registry *monkit.Registry) { 28 | if registry == nil { 29 | registry = monkit.Default 30 | } 31 | pkg := registry.Package() 32 | for _, source := range registrations { 33 | pkg.Chain(source) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /environment/os.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build (unix || windows) && !darwin 16 | // +build unix windows 17 | // +build !darwin 18 | 19 | package environment 20 | 21 | import ( 22 | "github.com/spacemonkeygo/monkit/v3" 23 | ) 24 | 25 | // OS returns a StatSource that includes various operating system process data 26 | // such as the number of file descriptors. Not expected to be called directly, 27 | // as this StatSource is added by Register. 28 | func OS() monkit.StatSource { 29 | return monkit.StatSourceFunc(func(cb func(key monkit.SeriesKey, field string, val float64)) { 30 | fds, err := fdCount() 31 | if err == nil { 32 | cb(monkit.NewSeriesKey("fds"), "count", float64(fds)) 33 | } 34 | }) 35 | } 36 | 37 | func init() { registrations = append(registrations, OS()) } 38 | -------------------------------------------------------------------------------- /environment/os_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build (unix || windows) && !darwin 16 | // +build unix windows 17 | // +build !darwin 18 | 19 | package environment 20 | 21 | import "testing" 22 | 23 | func TestFDCount(t *testing.T) { 24 | count, err := fdCount() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | if count == 0 { 29 | t.Fatal("fd count should not be zero") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /environment/os_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build unix && !darwin 16 | // +build unix,!darwin 17 | 18 | package environment 19 | 20 | import ( 21 | "io" 22 | "os" 23 | ) 24 | 25 | func fdCount() (count int, err error) { 26 | f, err := os.Open("/proc/self/fd") 27 | if err != nil { 28 | return 0, err 29 | } 30 | defer f.Close() 31 | for { 32 | names, err := f.Readdirnames(4096) 33 | count += len(names) 34 | if err != nil { 35 | if err == io.EOF { 36 | return count, nil 37 | } 38 | return count, err 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /environment/os_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environment 16 | 17 | import ( 18 | "syscall" 19 | "unsafe" 20 | ) 21 | 22 | var ( 23 | getProcessHandleCount = kernel32.MustFindProc("GetProcessHandleCount") 24 | ) 25 | 26 | func fdCount() (count int, err error) { 27 | h, err := syscall.GetCurrentProcess() 28 | if err != nil { 29 | return 0, err 30 | } 31 | var procCount uint32 32 | r1, _, err := getProcessHandleCount.Call( 33 | uintptr(h), uintptr(unsafe.Pointer(&procCount))) 34 | if r1 == 0 { 35 | // if r1 == 0, then GetProcessHandleCount failed and err will be set to 36 | // the formatted string for whatever GetLastError() returns. 37 | return 0, err 38 | } 39 | return int(procCount), nil 40 | } 41 | -------------------------------------------------------------------------------- /environment/proc.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environment 16 | 17 | import ( 18 | "github.com/spacemonkeygo/monkit/v3" 19 | ) 20 | 21 | // Proc returns a StatSource that includes various operating system process data 22 | // from /proc if available. Not expected to be called directly, as this StatSource 23 | // is added by Register. 24 | func Proc() monkit.StatSource { 25 | return monkit.StatSourceFunc(proc) 26 | } 27 | 28 | func init() { registrations = append(registrations, Proc()) } 29 | -------------------------------------------------------------------------------- /environment/proc_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environment 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/spacemonkeygo/monkit/v3" 22 | ) 23 | 24 | func proc(cb func(key monkit.SeriesKey, field string, val float64)) { 25 | var stat procSelfStat 26 | err := readProcSelfStat(&stat) 27 | if err == nil { 28 | monkit.StatSourceFromStruct(monkit.NewSeriesKey("proc_stat"), &stat).Stats(cb) 29 | } 30 | 31 | var statm procSelfStatm 32 | err = readProcSelfStatm(&statm) 33 | if err == nil { 34 | monkit.StatSourceFromStruct(monkit.NewSeriesKey("proc_statm"), &statm).Stats(cb) 35 | } 36 | } 37 | 38 | type procSelfStat struct { 39 | Pid int64 40 | Comm string 41 | State byte 42 | Ppid int64 43 | Pgrp int64 44 | Session int64 45 | TtyNr int64 46 | Tpgid int64 47 | Flags uint64 48 | Minflt uint64 49 | Cminflt uint64 50 | Majflt uint64 51 | Cmajflt uint64 52 | Utime uint64 53 | Stime uint64 54 | Cutime int64 55 | Cstime int64 56 | Priority int64 57 | Nice int64 58 | NumThreads int64 59 | Itrealvalue int64 60 | Starttime uint64 61 | Vsize uint64 62 | Rss int64 63 | Rsslim uint64 64 | Startcode uint64 65 | Endcode uint64 66 | Startstack uint64 67 | Kstkesp uint64 68 | Kstkeip uint64 69 | Signal uint64 70 | Blocked uint64 71 | Sigignore uint64 72 | Sigcatch uint64 73 | Wchan uint64 74 | Nswap uint64 75 | Cnswap uint64 76 | ExitSignal int64 77 | Processor int64 78 | RtPriority uint64 79 | Policy uint64 80 | DelayAcctBlkioTicks uint64 81 | GuestTime uint64 82 | CguestTime int64 83 | } 84 | 85 | func readProcSelfStat(s *procSelfStat) error { 86 | data, err := os.ReadFile("/proc/self/stat") 87 | if err != nil { 88 | return err 89 | } 90 | _, err = fmt.Sscanf(string(data), "%d %s %c %d %d %d %d %d %d %d %d "+ 91 | "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d "+ 92 | "%d %d %d %d %d %d %d %d %d %d", &s.Pid, &s.Comm, &s.State, &s.Ppid, 93 | &s.Pgrp, &s.Session, &s.TtyNr, &s.Tpgid, &s.Flags, &s.Minflt, &s.Cminflt, 94 | &s.Majflt, &s.Cmajflt, &s.Utime, &s.Stime, &s.Cutime, &s.Cstime, 95 | &s.Priority, &s.Nice, &s.NumThreads, &s.Itrealvalue, &s.Starttime, 96 | &s.Vsize, &s.Rss, &s.Rsslim, &s.Startcode, &s.Endcode, &s.Startstack, 97 | &s.Kstkesp, &s.Kstkeip, &s.Signal, &s.Blocked, &s.Sigignore, &s.Sigcatch, 98 | &s.Wchan, &s.Nswap, &s.Cnswap, &s.ExitSignal, &s.Processor, &s.RtPriority, 99 | &s.Policy, &s.DelayAcctBlkioTicks, &s.GuestTime, &s.CguestTime) 100 | return err 101 | } 102 | 103 | type procSelfStatm struct { 104 | Size int 105 | Resident int 106 | Share int 107 | Text int 108 | Lib int 109 | Data int 110 | Dt int 111 | } 112 | 113 | func readProcSelfStatm(s *procSelfStatm) error { 114 | data, err := os.ReadFile("/proc/self/statm") 115 | if err != nil { 116 | return err 117 | } 118 | _, err = fmt.Sscanf(string(data), "%d %d %d %d %d %d %d", &s.Size, 119 | &s.Resident, &s.Share, &s.Text, &s.Lib, &s.Data, &s.Dt) 120 | return err 121 | } 122 | -------------------------------------------------------------------------------- /environment/proc_other.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !linux 16 | // +build !linux 17 | 18 | package environment 19 | 20 | import "github.com/spacemonkeygo/monkit/v3" 21 | 22 | func proc(cb func(series monkit.SeriesKey, field string, val float64)) {} 23 | -------------------------------------------------------------------------------- /environment/process.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build unix || windows 16 | // +build unix windows 17 | 18 | package environment 19 | 20 | import ( 21 | "hash/crc32" 22 | "io" 23 | "sync" 24 | "time" 25 | 26 | "github.com/spacemonkeygo/monkit/v3" 27 | "github.com/spacemonkeygo/monkit/v3/monotime" 28 | ) 29 | 30 | var ( 31 | startTime = monotime.Now() 32 | ) 33 | 34 | // Process returns a StatSource including generic process data, such as 35 | // the process uptime, and a crc of the executing binary if possible. Also 36 | // includes a 'control' value so data collectors can accurately count how many 37 | // unique running processes being monitored there are. Not expected to be 38 | // called directly, as this StatSource is added by Register. 39 | func Process() monkit.StatSource { 40 | return monkit.StatSourceFunc(func(cb func(key monkit.SeriesKey, field string, val float64)) { 41 | cb(monkit.NewSeriesKey("process"), "control", 1) 42 | c, err := processCRC() 43 | if err == nil { 44 | cb(monkit.NewSeriesKey("process"), "crc", float64(c)) 45 | } 46 | cb(monkit.NewSeriesKey("process"), "uptime", time.Since(startTime).Seconds()) 47 | }) 48 | } 49 | 50 | var crcCache struct { 51 | once sync.Once 52 | crc uint32 53 | err error 54 | } 55 | 56 | func processCRC() (uint32, error) { 57 | crcCache.once.Do(func() { 58 | crcCache.crc, crcCache.err = getProcessCRC() 59 | }) 60 | return crcCache.crc, crcCache.err 61 | } 62 | 63 | func getProcessCRC() (uint32, error) { 64 | fh, err := openProc() 65 | if err != nil { 66 | return 0, err 67 | } 68 | defer fh.Close() 69 | c := crc32.NewIEEE() 70 | _, err = io.Copy(c, fh) 71 | return c.Sum32(), err 72 | } 73 | 74 | func init() { registrations = append(registrations, Process()) } 75 | -------------------------------------------------------------------------------- /environment/process_cc_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !cgo 16 | // +build !cgo 17 | 18 | package environment 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | ) 24 | 25 | func openProc() (*os.File, error) { 26 | return nil, fmt.Errorf("openProc unimplemented for darwin cross compilation") 27 | } 28 | -------------------------------------------------------------------------------- /environment/process_cgo_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build cgo 16 | // +build cgo 17 | 18 | package environment 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | "unsafe" 24 | ) 25 | 26 | // #include 27 | // #include 28 | import "C" 29 | 30 | func openProc() (*os.File, error) { 31 | const bufsize = 4096 32 | 33 | buf := (*C.char)(C.malloc(bufsize)) 34 | defer C.free(unsafe.Pointer(buf)) 35 | 36 | size := C.uint32_t(bufsize) 37 | if rc := C._NSGetExecutablePath(buf, &size); rc != 0 { 38 | return nil, fmt.Errorf("error in cgo call to get path: %d", rc) 39 | } 40 | 41 | return os.Open(C.GoString(buf)) 42 | } 43 | -------------------------------------------------------------------------------- /environment/process_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build (unix || windows) && !darwin 16 | // +build unix windows 17 | // +build !darwin 18 | 19 | package environment 20 | 21 | import ( 22 | "io" 23 | "testing" 24 | ) 25 | 26 | func TestOpenProc(t *testing.T) { 27 | f, err := openProc() 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | defer f.Close() 32 | 33 | all, err := io.ReadAll(f) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if len(all) == 0 { 38 | t.Fatal("proc bytes should not be empty") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /environment/process_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build unix && !darwin 16 | // +build unix,!darwin 17 | 18 | package environment 19 | 20 | import ( 21 | "os" 22 | ) 23 | 24 | func openProc() (*os.File, error) { 25 | return os.Open("/proc/self/exe") 26 | } 27 | -------------------------------------------------------------------------------- /environment/process_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package environment 16 | 17 | import ( 18 | "os" 19 | "syscall" 20 | "unsafe" 21 | ) 22 | 23 | var ( 24 | kernel32 = syscall.MustLoadDLL("kernel32.dll") 25 | getModuleFileName = kernel32.MustFindProc("GetModuleFileNameW") 26 | ) 27 | 28 | func openProc() (*os.File, error) { 29 | m16 := make([]uint16, 1024) 30 | r1, _, err := getModuleFileName.Call(0, 31 | uintptr(unsafe.Pointer(&m16[0])), uintptr(len(m16))) 32 | if r1 == 0 { 33 | // if r1 == 0, then GetModuleFileNameW failed and err will be set to 34 | // the formatted string for whatever GetLastError() returns. 35 | return nil, err 36 | } 37 | m := syscall.UTF16ToString(m16) 38 | return os.Open(m) 39 | } 40 | -------------------------------------------------------------------------------- /environment/runtime.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !tinygo 16 | // +build !tinygo 17 | 18 | package environment 19 | 20 | import ( 21 | "runtime" 22 | "runtime/debug" 23 | 24 | "github.com/spacemonkeygo/monkit/v3" 25 | ) 26 | 27 | // Runtime returns a StatSource that includes information gathered from the 28 | // Go runtime, including the number of goroutines currently running, and 29 | // other live memory data. Not expected to be called directly, as this 30 | // StatSource is added by Register. 31 | func Runtime() monkit.StatSource { 32 | durDist := monkit.NewDurationDist(monkit.NewSeriesKey("runtime_gcstats")) 33 | lastNumGC := int64(0) 34 | 35 | return monkit.StatSourceFunc(func(cb func(key monkit.SeriesKey, field string, val float64)) { 36 | cb(monkit.NewSeriesKey("goroutines"), "count", float64(runtime.NumGoroutine())) 37 | 38 | { 39 | var stats runtime.MemStats 40 | runtime.ReadMemStats(&stats) 41 | monkit.StatSourceFromStruct(monkit.NewSeriesKey("runtime_memstats"), stats).Stats(cb) 42 | } 43 | 44 | { 45 | var stats debug.GCStats 46 | debug.ReadGCStats(&stats) 47 | if lastNumGC != stats.NumGC && len(stats.Pause) > 0 { 48 | durDist.Insert(stats.Pause[0]) 49 | } 50 | durDist.Stats(cb) 51 | } 52 | }) 53 | } 54 | 55 | func init() { registrations = append(registrations, Runtime()) } 56 | -------------------------------------------------------------------------------- /environment/rusage.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build unix 16 | // +build unix 17 | 18 | package environment 19 | 20 | import ( 21 | "syscall" 22 | 23 | "github.com/spacemonkeygo/monkit/v3" 24 | ) 25 | 26 | // Rusage returns a StatSource that provides as many statistics as possible 27 | // gathered from the Rusage syscall. Not expected to be called directly, as 28 | // this StatSource is added by Register. 29 | func Rusage() monkit.StatSource { 30 | return monkit.StatSourceFunc(func(cb func(key monkit.SeriesKey, field string, val float64)) { 31 | var rusage syscall.Rusage 32 | err := syscall.Getrusage(syscall.RUSAGE_SELF, &rusage) 33 | if err == nil { 34 | monkit.StatSourceFromStruct(monkit.NewSeriesKey("rusage"), &rusage).Stats(cb) 35 | } 36 | }) 37 | } 38 | 39 | func init() { registrations = append(registrations, Rusage()) } 40 | -------------------------------------------------------------------------------- /error_names.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "context" 19 | "io" 20 | "sync" 21 | "sync/atomic" 22 | ) 23 | 24 | // errorNameHandlers keeps track of the list of error name handlers monkit will 25 | // call to give errors good metric names. 26 | var errorNameHandlers struct { 27 | write_mu sync.Mutex 28 | value atomic.Value 29 | } 30 | 31 | // AddErrorNameHandler adds an error name handler function that will be 32 | // consulted every time an error is captured for a task. The handlers will be 33 | // called in the order they were registered with the most recently added 34 | // handler first, until a handler returns true for the second return value. 35 | // If no handler returns true, the error is checked to see if it implements 36 | // an interface that allows it to name itself, and otherwise, monkit attempts 37 | // to find a good name for most built in Go standard library errors. 38 | func AddErrorNameHandler(f func(error) (string, bool)) { 39 | errorNameHandlers.write_mu.Lock() 40 | defer errorNameHandlers.write_mu.Unlock() 41 | 42 | handlers, _ := errorNameHandlers.value.Load().([]func(error) (string, bool)) 43 | handlers = append(handlers, f) 44 | errorNameHandlers.value.Store(handlers) 45 | } 46 | 47 | // getErrorName implements the logic described in the AddErrorNameHandler 48 | // function. 49 | func getErrorName(err error) string { 50 | // check if any of the handlers will handle it 51 | handlers, _ := errorNameHandlers.value.Load().([]func(error) (string, bool)) 52 | for i := len(handlers) - 1; i >= 0; i-- { 53 | if name, ok := handlers[i](err); ok { 54 | return name 55 | } 56 | } 57 | 58 | // check if it knows how to name itself 59 | type namer interface { 60 | Name() (string, bool) 61 | } 62 | 63 | if n, ok := err.(namer); ok { 64 | if name, ok := n.Name(); ok { 65 | return name 66 | } 67 | } 68 | 69 | // check if it's a known error that we handle to give good names 70 | switch err { 71 | case io.EOF: 72 | return "EOF" 73 | case io.ErrUnexpectedEOF: 74 | return "Unexpected EOF Error" 75 | case io.ErrClosedPipe: 76 | return "Closed Pipe Error" 77 | case io.ErrNoProgress: 78 | return "No Progress Error" 79 | case io.ErrShortBuffer: 80 | return "Short Buffer Error" 81 | case io.ErrShortWrite: 82 | return "Short Write Error" 83 | case context.Canceled: 84 | return "Canceled" 85 | case context.DeadlineExceeded: 86 | return "Timeout" 87 | } 88 | if isErrnoError(err) { 89 | return "Errno" 90 | } 91 | 92 | if name := getNetErrorName(err); name != "" { 93 | return name 94 | } 95 | 96 | return "System Error" 97 | } 98 | -------------------------------------------------------------------------------- /error_names_ae.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build appengine 16 | // +build appengine 17 | 18 | package monkit 19 | 20 | func isErrnoError(err error) bool { 21 | return false 22 | } 23 | -------------------------------------------------------------------------------- /error_names_net.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !tinygo 16 | // +build !tinygo 17 | 18 | package monkit 19 | 20 | import ( 21 | "net" 22 | "os" 23 | ) 24 | 25 | // getNetErrorName translates net package error. 26 | func getNetErrorName(err error) string { 27 | switch err.(type) { 28 | case *os.SyscallError: 29 | case net.UnknownNetworkError: 30 | return "Unknown Network Error" 31 | case *net.AddrError: 32 | return "Addr Error" 33 | case net.InvalidAddrError: 34 | return "Invalid Addr Error" 35 | case *net.OpError: 36 | return "Net Op Error" 37 | case *net.ParseError: 38 | return "Net Parse Error" 39 | case *net.DNSError: 40 | return "DNS Error" 41 | case *net.DNSConfigError: 42 | return "DNS Config Error" 43 | case net.Error: 44 | return "Network Error" 45 | } 46 | return "" 47 | } 48 | -------------------------------------------------------------------------------- /error_names_net_tinygo.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build tinygo 16 | // +build tinygo 17 | 18 | package monkit 19 | 20 | import ( 21 | "net" 22 | "os" 23 | ) 24 | 25 | // getNetErrorName translates net package error. 26 | func getNetErrorName(err error) string { 27 | // tiny-go does not implement the full net package, 28 | // hence it needs special handling. 29 | 30 | switch err.(type) { 31 | case *os.SyscallError: 32 | // case net.UnknownNetworkError: 33 | // return "Unknown Network Error" 34 | case *net.AddrError: 35 | return "Addr Error" 36 | // case net.InvalidAddrError: 37 | // return "Invalid Addr Error" 38 | case *net.OpError: 39 | return "Net Op Error" 40 | case *net.ParseError: 41 | return "Net Parse Error" 42 | // case *net.DNSError: 43 | // return "DNS Error" 44 | // case *net.DNSConfigError: 45 | // return "DNS Config Error" 46 | case net.Error: 47 | return "Network Error" 48 | } 49 | return "" 50 | } 51 | -------------------------------------------------------------------------------- /error_names_syscall.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !appengine 16 | // +build !appengine 17 | 18 | package monkit 19 | 20 | import "syscall" 21 | 22 | func isErrnoError(err error) bool { 23 | _, ok := err.(syscall.Errno) 24 | return ok 25 | } 26 | -------------------------------------------------------------------------------- /examples/minimal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/spacemonkeygo/monkit/v3" 11 | "github.com/spacemonkeygo/monkit/v3/environment" 12 | "github.com/spacemonkeygo/monkit/v3/present" 13 | ) 14 | 15 | var mon = monkit.Package() 16 | 17 | func main() { 18 | environment.Register(monkit.Default) 19 | 20 | go http.ListenAndServe("127.0.0.1:9000", present.HTTP(monkit.Default)) 21 | 22 | for { 23 | time.Sleep(100 * time.Millisecond) 24 | if err := DoStuff(context.Background()); err != nil { 25 | fmt.Println("error", err) 26 | } 27 | } 28 | } 29 | 30 | func DoStuff(ctx context.Context) (err error) { 31 | defer mon.Task()(&ctx, "query", []interface{}{[]byte{1, 2, 3}, "args", time.Now()})(&err) 32 | 33 | result, err := ComputeThing(ctx, 1, 2) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | fmt.Println(result) 39 | return nil 40 | } 41 | 42 | func ComputeThing(ctx context.Context, arg1, arg2 int) (res int, err error) { 43 | defer mon.Task()(&ctx)(&err) 44 | 45 | timer := mon.Timer("subcomputation").Start() 46 | res = arg1 + arg2 47 | timer.Stop() 48 | 49 | if res == 3 { 50 | mon.Event("hit 3") 51 | } 52 | 53 | mon.RawVal("raw").Observe(1.0) 54 | mon.RawValk(monkit.NewSeriesKey("rawk").WithTag("foo", "bar"), monkit.Sum, monkit.Count).Observe(1.0) 55 | mon.BoolVal("was-4").Observe(res == 4) 56 | mon.IntVal("res").Observe(int64(res)) 57 | mon.DurationVal("took").Observe(time.Second + time.Duration(rand.Intn(int(10*time.Second)))) 58 | mon.Counter("calls").Inc(1) 59 | mon.Gauge("arg1", func() float64 { return float64(arg1) }) 60 | mon.Meter("arg2").Mark(arg2) 61 | 62 | time.Sleep(time.Second) 63 | 64 | return arg1 + arg2, nil 65 | } 66 | -------------------------------------------------------------------------------- /examples/wasm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spacemonkeygo/monkit/v3" 7 | "github.com/spacemonkeygo/monkit/v3/environment" 8 | ) 9 | 10 | var mon = monkit.Package() 11 | 12 | func main() { 13 | ctx := context.Background() 14 | environment.Register(monkit.Default) 15 | 16 | var err error 17 | defer mon.Task()(&ctx)(&err) 18 | } 19 | -------------------------------------------------------------------------------- /floatdist.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // WARNING: THE NON-M4 VERSIONS OF THIS FILE ARE GENERATED BY GO GENERATE! 16 | // ONLY MAKE CHANGES TO THE M4 FILE 17 | // 18 | 19 | package monkit 20 | 21 | import ( 22 | "sort" 23 | ) 24 | 25 | // FloatDist keeps statistics about values such as 26 | // low/high/recent/average/quantiles. Not threadsafe. Construct with 27 | // NewFloatDist(). Fields are expected to be read from but not written to. 28 | type FloatDist struct { 29 | // Low and High are the lowest and highest values observed since 30 | // construction or the last reset. 31 | Low, High float64 32 | 33 | // Recent is the last observed value. 34 | Recent float64 35 | 36 | // Count is the number of observed values since construction or the last 37 | // reset. 38 | Count int64 39 | 40 | // Sum is the sum of all the observed values since construction or the last 41 | // reset. 42 | Sum float64 43 | 44 | key SeriesKey 45 | reservoir [ReservoirSize]float32 46 | rng xorshift128 47 | sorted bool 48 | } 49 | 50 | func initFloatDist(v *FloatDist, key SeriesKey) { 51 | v.key = key 52 | v.rng = newXORShift128() 53 | } 54 | 55 | // NewFloatDist creates a distribution of float64s. 56 | func NewFloatDist(key SeriesKey) (d *FloatDist) { 57 | d = &FloatDist{} 58 | initFloatDist(d, key) 59 | return d 60 | } 61 | 62 | // Insert adds a value to the distribution, updating appropriate values. 63 | func (d *FloatDist) Insert(val float64) { 64 | if d.Count != 0 { 65 | if val < d.Low { 66 | d.Low = val 67 | } 68 | if val > d.High { 69 | d.High = val 70 | } 71 | } else { 72 | d.Low = val 73 | d.High = val 74 | } 75 | d.Recent = val 76 | d.Sum += val 77 | 78 | index := d.Count 79 | d.Count += 1 80 | 81 | if index < ReservoirSize { 82 | d.reservoir[index] = float32(val) 83 | d.sorted = false 84 | } else { 85 | window := d.Count 86 | // careful, the capitalization of Window is important 87 | if Window > 0 && window > Window { 88 | window = Window 89 | } 90 | // fast, but kind of biased. probably okay 91 | j := d.rng.Uint64() % uint64(window) 92 | if j < ReservoirSize { 93 | d.reservoir[int(j)] = float32(val) 94 | d.sorted = false 95 | } 96 | } 97 | } 98 | 99 | // FullAverage calculates and returns the average of all inserted values. 100 | func (d *FloatDist) FullAverage() float64 { 101 | if d.Count > 0 { 102 | return d.Sum / float64(d.Count) 103 | } 104 | return 0 105 | } 106 | 107 | // ReservoirAverage calculates the average of the current reservoir. 108 | func (d *FloatDist) ReservoirAverage() float64 { 109 | amount := ReservoirSize 110 | if d.Count < int64(amount) { 111 | amount = int(d.Count) 112 | } 113 | if amount <= 0 { 114 | return 0 115 | } 116 | var sum float32 117 | for i := 0; i < amount; i++ { 118 | sum += d.reservoir[i] 119 | } 120 | return float64(sum / float32(amount)) 121 | } 122 | 123 | // Query will return the approximate value at the given quantile from the 124 | // reservoir, where 0 <= quantile <= 1. 125 | func (d *FloatDist) Query(quantile float64) float64 { 126 | rlen := int(ReservoirSize) 127 | if int64(rlen) > d.Count { 128 | rlen = int(d.Count) 129 | } 130 | 131 | if rlen < 2 { 132 | return float64(d.reservoir[0]) 133 | } 134 | 135 | reservoir := d.reservoir[:rlen] 136 | if !d.sorted { 137 | sort.Sort(float32Slice(reservoir)) 138 | d.sorted = true 139 | } 140 | 141 | if quantile <= 0 { 142 | return float64(reservoir[0]) 143 | } 144 | if quantile >= 1 { 145 | return float64(reservoir[rlen-1]) 146 | } 147 | 148 | idx_float := quantile * float64(rlen-1) 149 | idx := int(idx_float) 150 | 151 | diff := idx_float - float64(idx) 152 | prior := float64(reservoir[idx]) 153 | return float64(prior + diff*(float64(reservoir[idx+1])-prior)) 154 | } 155 | 156 | // Copy returns a full copy of the entire distribution. 157 | func (d *FloatDist) Copy() *FloatDist { 158 | cp := *d 159 | cp.rng = newXORShift128() 160 | return &cp 161 | } 162 | 163 | func (d *FloatDist) Reset() { 164 | d.Low, d.High, d.Recent, d.Count, d.Sum = 0, 0, 0, 0, 0 165 | // resetting count will reset the quantile reservoir 166 | } 167 | 168 | func (d *FloatDist) Stats(cb func(key SeriesKey, field string, val float64)) { 169 | count := d.Count 170 | cb(d.key, "count", float64(count)) 171 | if count > 0 { 172 | cb(d.key, "sum", d.toFloat64(d.Sum)) 173 | cb(d.key, "min", d.toFloat64(d.Low)) 174 | cb(d.key, "max", d.toFloat64(d.High)) 175 | cb(d.key, "rmin", d.toFloat64(d.Query(0))) 176 | cb(d.key, "ravg", d.toFloat64(d.ReservoirAverage())) 177 | cb(d.key, "r10", d.toFloat64(d.Query(.1))) 178 | cb(d.key, "r50", d.toFloat64(d.Query(.5))) 179 | cb(d.key, "r90", d.toFloat64(d.Query(.9))) 180 | cb(d.key, "r99", d.toFloat64(d.Query(.99))) 181 | cb(d.key, "rmax", d.toFloat64(d.Query(1))) 182 | cb(d.key, "recent", d.toFloat64(d.Recent)) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /func.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | // Func represents a FuncStats bound to a particular function id, scope, and 22 | // name. You should create a Func using the Func creation methods 23 | // (Func/FuncNamed) on a Scope. If you want to manage installation bookkeeping 24 | // yourself, create a FuncStats directly. Expected Func creation like: 25 | // 26 | // var mon = monkit.Package() 27 | // 28 | // func MyFunc() { 29 | // f := mon.Func() 30 | // ... 31 | // } 32 | type Func struct { 33 | // sync/atomic things 34 | FuncStats 35 | 36 | // constructor things 37 | id int64 38 | scope *Scope 39 | key SeriesKey 40 | } 41 | 42 | func newFunc(s *Scope, key SeriesKey) (f *Func) { 43 | f = &Func{ 44 | id: NewId(), 45 | scope: s, 46 | key: key, 47 | } 48 | initFuncStats(&f.FuncStats, key) 49 | return f 50 | } 51 | 52 | // ShortName returns the name of the function within the package 53 | func (f *Func) ShortName() string { return f.key.Tags.Get("name") } 54 | 55 | // FullName returns the name of the function including the package 56 | func (f *Func) FullName() string { 57 | return fmt.Sprintf("%s.%s", f.scope.name, f.key.Tags.Get("name")) 58 | } 59 | 60 | // Id returns a unique integer referencing this function 61 | func (f *Func) Id() int64 { return f.id } 62 | 63 | // Scope references the Scope this Func is bound to 64 | func (f *Func) Scope() *Scope { return f.scope } 65 | 66 | // Parents will call the given cb with all of the unique Funcs that so far 67 | // have called this Func. 68 | func (f *Func) Parents(cb func(f *Func)) { 69 | f.FuncStats.parents(cb) 70 | } 71 | -------------------------------------------------------------------------------- /func_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 Storj Labs, Inc. 2 | // Copyright (C) 2015 Space Monkey, Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package monkit 17 | 18 | import "testing" 19 | 20 | func TestFuncName(t *testing.T) { 21 | f := Default.Package().Func() 22 | if f.ShortName() != "TestFuncName" { 23 | t.Fatal("invalid short name:", f.ShortName()) 24 | } 25 | if f.FullName() != "github.com/spacemonkeygo/monkit/v3.TestFuncName" { 26 | t.Fatal("invalid full name:", f.FullName()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /funcset.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "sync" 19 | ) 20 | 21 | // funcSet is a set data structure (keeps track of unique functions). funcSet 22 | // has a fast path for dealing with cases where the set only has one element. 23 | // 24 | // to reduce memory usage for functions, funcSet exposes its mutex for use in 25 | // other contexts 26 | type funcSet struct { 27 | // sync/atomic things 28 | first *Func 29 | 30 | // protected by mtx 31 | sync.Mutex 32 | rest map[*Func]struct{} 33 | } 34 | 35 | var ( 36 | // used to signify that we've specifically added a nil function, since nil is 37 | // used internally to specify an empty set. 38 | nilFunc = &Func{} 39 | ) 40 | 41 | func (s *funcSet) Add(f *Func) { 42 | if f == nil { 43 | f = nilFunc 44 | } 45 | if loadFunc(&s.first) == f { 46 | return 47 | } 48 | if compareAndSwapFunc(&s.first, nil, f) { 49 | return 50 | } 51 | s.Mutex.Lock() 52 | if s.rest == nil { 53 | s.rest = map[*Func]struct{}{} 54 | } 55 | s.rest[f] = struct{}{} 56 | s.Mutex.Unlock() 57 | } 58 | 59 | // Iterate loops over all unique elements of the set. 60 | func (s *funcSet) Iterate(cb func(f *Func)) { 61 | s.Mutex.Lock() 62 | uniq := make(map[*Func]struct{}, len(s.rest)+1) 63 | for f := range s.rest { 64 | uniq[f] = struct{}{} 65 | } 66 | s.Mutex.Unlock() 67 | f := loadFunc(&s.first) 68 | if f != nil { 69 | uniq[f] = struct{}{} 70 | } 71 | for f := range uniq { 72 | if f == nilFunc { 73 | cb(nil) 74 | } else { 75 | cb(f) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /funcstats.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "sync/atomic" 19 | "time" 20 | 21 | "github.com/spacemonkeygo/monkit/v3/monotime" 22 | ) 23 | 24 | // FuncStats keeps track of statistics about a possible function's execution. 25 | // Should be created with NewFuncStats, though expected creation is through a 26 | // Func object: 27 | // 28 | // var mon = monkit.Package() 29 | // 30 | // func MyFunc() { 31 | // f := mon.Func() 32 | // ... 33 | // } 34 | type FuncStats struct { 35 | // sync/atomic things 36 | current int64 37 | highwater int64 38 | parentsAndMutex funcSet 39 | 40 | // mutex things (reuses mutex from parents) 41 | errors map[string]int64 42 | panics int64 43 | successTimes DurationDist 44 | failureTimes DurationDist 45 | key SeriesKey 46 | } 47 | 48 | func initFuncStats(f *FuncStats, key SeriesKey) { 49 | f.key = key 50 | f.errors = map[string]int64{} 51 | 52 | key.Measurement += "_times" 53 | initDurationDist(&f.successTimes, key.WithTag("kind", "success")) 54 | initDurationDist(&f.failureTimes, key.WithTag("kind", "failure")) 55 | } 56 | 57 | // NewFuncStats creates a FuncStats 58 | func NewFuncStats(key SeriesKey) (f *FuncStats) { 59 | f = &FuncStats{} 60 | initFuncStats(f, key) 61 | return f 62 | } 63 | 64 | // Reset resets all recorded data. 65 | func (f *FuncStats) Reset() { 66 | atomic.StoreInt64(&f.current, 0) 67 | atomic.StoreInt64(&f.highwater, 0) 68 | f.parentsAndMutex.Lock() 69 | f.errors = make(map[string]int64, len(f.errors)) 70 | f.panics = 0 71 | f.successTimes.Reset() 72 | f.failureTimes.Reset() 73 | f.parentsAndMutex.Unlock() 74 | } 75 | 76 | func (f *FuncStats) start(parent *Func) { 77 | f.parentsAndMutex.Add(parent) 78 | current := atomic.AddInt64(&f.current, 1) 79 | for { 80 | highwater := atomic.LoadInt64(&f.highwater) 81 | if current <= highwater || 82 | atomic.CompareAndSwapInt64(&f.highwater, highwater, current) { 83 | break 84 | } 85 | } 86 | } 87 | 88 | func (f *FuncStats) end(err error, panicked bool, duration time.Duration) { 89 | atomic.AddInt64(&f.current, -1) 90 | f.parentsAndMutex.Lock() 91 | if panicked { 92 | f.panics += 1 93 | f.failureTimes.Insert(duration) 94 | f.parentsAndMutex.Unlock() 95 | return 96 | } 97 | if err == nil { 98 | f.successTimes.Insert(duration) 99 | f.parentsAndMutex.Unlock() 100 | return 101 | } 102 | f.failureTimes.Insert(duration) 103 | f.errors[getErrorName(err)] += 1 104 | f.parentsAndMutex.Unlock() 105 | } 106 | 107 | // Current returns how many concurrent instances of this function are currently 108 | // being observed. 109 | func (f *FuncStats) Current() int64 { return atomic.LoadInt64(&f.current) } 110 | 111 | // Highwater returns the highest value Current() would ever return. 112 | func (f *FuncStats) Highwater() int64 { return atomic.LoadInt64(&f.highwater) } 113 | 114 | // Success returns the number of successes that have been observed 115 | func (f *FuncStats) Success() (rv int64) { 116 | f.parentsAndMutex.Lock() 117 | rv = f.successTimes.Count 118 | f.parentsAndMutex.Unlock() 119 | return rv 120 | } 121 | 122 | // Panics returns the number of panics that have been observed 123 | func (f *FuncStats) Panics() (rv int64) { 124 | f.parentsAndMutex.Lock() 125 | rv = f.panics 126 | f.parentsAndMutex.Unlock() 127 | return rv 128 | } 129 | 130 | // Errors returns the number of errors observed by error type. The error type 131 | // is determined by handlers from AddErrorNameHandler, or a default that works 132 | // with most error types. 133 | func (f *FuncStats) Errors() (rv map[string]int64) { 134 | f.parentsAndMutex.Lock() 135 | rv = make(map[string]int64, len(f.errors)) 136 | for errname, count := range f.errors { 137 | rv[errname] = count 138 | } 139 | f.parentsAndMutex.Unlock() 140 | return rv 141 | } 142 | 143 | func (f *FuncStats) parents(cb func(f *Func)) { 144 | f.parentsAndMutex.Iterate(cb) 145 | } 146 | 147 | // Stats implements the StatSource interface 148 | func (f *FuncStats) Stats(cb func(key SeriesKey, field string, val float64)) { 149 | cb(f.key, "current", float64(f.Current())) 150 | cb(f.key, "highwater", float64(f.Highwater())) 151 | 152 | f.parentsAndMutex.Lock() 153 | panics := f.panics 154 | errs := make(map[string]int64, len(f.errors)) 155 | for errname, count := range f.errors { 156 | errs[errname] = count 157 | } 158 | st := f.successTimes.Copy() 159 | ft := f.failureTimes.Copy() 160 | f.parentsAndMutex.Unlock() 161 | 162 | cb(f.key, "successes", float64(st.Count)) 163 | e_count := int64(0) 164 | for errname, count := range errs { 165 | e_count += count 166 | cb(f.key.WithTag("error_name", errname), "count", float64(count)) 167 | } 168 | cb(f.key, "errors", float64(e_count)) 169 | cb(f.key, "panics", float64(panics)) 170 | cb(f.key, "failures", float64(e_count+panics)) 171 | cb(f.key, "total", float64(st.Count+e_count+panics)) 172 | 173 | st.Stats(cb) 174 | ft.Stats(cb) 175 | } 176 | 177 | // SuccessTimes returns a DurationDist of successes 178 | func (f *FuncStats) SuccessTimes() *DurationDist { 179 | f.parentsAndMutex.Lock() 180 | d := f.successTimes.Copy() 181 | f.parentsAndMutex.Unlock() 182 | return d 183 | } 184 | 185 | // FailureTimes returns a DurationDist of failures (includes panics and errors) 186 | func (f *FuncStats) FailureTimes() *DurationDist { 187 | f.parentsAndMutex.Lock() 188 | d := f.failureTimes.Copy() 189 | f.parentsAndMutex.Unlock() 190 | return d 191 | } 192 | 193 | // Observe starts the stopwatch for observing this function and returns a 194 | // function to be called at the end of the function execution. Expected usage 195 | // like: 196 | // 197 | // func MyFunc() (err error) { 198 | // defer funcStats.Observe()(&err) 199 | // ... 200 | // } 201 | func (f *FuncStats) Observe() func(errptr *error) { 202 | f.start(nil) 203 | start := monotime.Now() 204 | return func(errptr *error) { 205 | rec := recover() 206 | panicked := rec != nil 207 | finish := monotime.Now() 208 | var err error 209 | if errptr != nil { 210 | err = *errptr 211 | } 212 | f.end(err, panicked, finish.Sub(start)) 213 | if panicked { 214 | panic(rec) 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spacemonkeygo/monkit/v3 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemonkeygo/monkit/a1546ad16946791e15c7dcc6199dc35a0d8f98c9/go.sum -------------------------------------------------------------------------------- /http/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Storj Labs, Inc. 2 | // See LICENSE for copying information. 3 | 4 | package http 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | 11 | "github.com/spacemonkeygo/monkit/v3" 12 | ) 13 | 14 | // TraceRequest will perform an HTTP request, creating a new Span for the HTTP 15 | // request and sending the Span in the HTTP request headers. 16 | // Compare to http.Client.Do. 17 | func TraceRequest(ctx context.Context, scope *monkit.Scope, cl Client, req *http.Request) ( 18 | resp *http.Response, err error) { 19 | defer scope.TaskNamed(req.Method)(&ctx)(&err) 20 | 21 | s := monkit.SpanFromCtx(ctx) 22 | s.Annotate("http.uri", req.URL.String()) 23 | TraceInfoFromSpan(s).SetHeader(req.Header) 24 | resp, err = cl.Do(req) 25 | if err != nil { 26 | return resp, err 27 | } 28 | s.Annotate("http.responsecode", fmt.Sprint(resp.StatusCode)) 29 | return resp, nil 30 | } 31 | -------------------------------------------------------------------------------- /http/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Storj Labs, Inc. 2 | // See LICENSE for copying information. 3 | 4 | package http 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/http" 12 | "sort" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/spacemonkeygo/monkit/v3" 18 | "github.com/spacemonkeygo/monkit/v3/present" 19 | ) 20 | 21 | type caller func(ctx context.Context, request *http.Request) (*http.Response, error) 22 | 23 | func TestPropagation(t *testing.T) { 24 | mon := monkit.Package() 25 | 26 | addr, closeServer := startHTTPServer(t) 27 | 28 | defer closeServer() 29 | 30 | ctx := context.Background() 31 | trace := monkit.NewTrace(monkit.NewId()) 32 | trace.Set(present.SampledKey, true) 33 | 34 | defer mon.Func().RemoteTrace(&ctx, 0, trace)(nil) 35 | 36 | body, header := clientCallWithRetry(t, ctx, addr, func(ctx context.Context, request *http.Request) (*http.Response, error) { 37 | return TraceRequest(ctx, monkit.ScopeNamed("client"), http.DefaultClient, request) 38 | }) 39 | 40 | s := monkit.SpanFromCtx(ctx) 41 | 42 | expected := fmt.Sprintf("%d/hello/true (http.uri=/)", s.Id()) 43 | 44 | if string(body) != expected { 45 | t.Fatalf("%s!=%s", string(body), expected) 46 | } 47 | if header != "" { 48 | t.Fatalf("tracestate should be empty: %s", header) 49 | } 50 | } 51 | 52 | func TestBaggage(t *testing.T) { 53 | mon := monkit.Package() 54 | 55 | addr, closeServer := startHTTPServer(t) 56 | 57 | defer closeServer() 58 | 59 | ctx := context.Background() 60 | trace := monkit.NewTrace(monkit.NewId()) 61 | trace.Set(present.SampledKey, true) 62 | 63 | defer mon.Func().RemoteTrace(&ctx, 0, trace)(nil) 64 | 65 | body, header := clientCallWithRetry(t, ctx, addr, func(ctx context.Context, request *http.Request) (*http.Response, error) { 66 | request.Header.Set(baggageHeader, "k=v") 67 | return TraceRequest(ctx, monkit.ScopeNamed("client"), http.DefaultClient, request) 68 | }) 69 | 70 | s := monkit.SpanFromCtx(ctx) 71 | 72 | expected := fmt.Sprintf("%d/hello/true (http.uri=/,k=v)", s.Id()) 73 | 74 | if string(body) != expected { 75 | t.Fatalf("%q!=%q", string(body), expected) 76 | } 77 | if header != "" { 78 | t.Fatalf("tracestate should be empty: %s", header) 79 | } 80 | } 81 | 82 | // TestForcedSample checks if sampling can be turned on without having trace/span on client side. 83 | func TestForcedSample(t *testing.T) { 84 | addr, closeServer := startHTTPServer(t) 85 | 86 | defer closeServer() 87 | 88 | body, header := clientCallWithRetry(t, context.Background(), addr, func(ctx context.Context, request *http.Request) (*http.Response, error) { 89 | request.Header.Set(traceStateHeader, "sampled=true") 90 | return http.DefaultClient.Do(request) 91 | }) 92 | 93 | expected := "0/hello/true (http.uri=/)" 94 | 95 | if string(body) != expected { 96 | t.Fatalf("%q!=%q", string(body), expected) 97 | } 98 | if header == "" { 99 | t.Fatalf("tracestate should not be empty: %s", header) 100 | } 101 | } 102 | 103 | func clientCallWithRetry(t *testing.T, ctx context.Context, addr string, caller caller) (string, string) { 104 | var err error 105 | for i := 0; i < 100; i++ { 106 | body, header, err := clientCall(ctx, addr, caller) 107 | if err == nil { 108 | return body, header 109 | } 110 | time.Sleep(50 * time.Millisecond) 111 | } 112 | t.Fatal(err) 113 | return "", "" 114 | } 115 | 116 | func clientCall(ctx context.Context, addr string, caller caller) (string, string, error) { 117 | request, err := http.NewRequest("GET", addr, nil) 118 | if err != nil { 119 | return "", "", err 120 | } 121 | 122 | resp, err := caller(ctx, request) 123 | if err != nil { 124 | return "", "", err 125 | } 126 | defer resp.Body.Close() 127 | body, err := io.ReadAll(resp.Body) 128 | if err != nil { 129 | return "", "", err 130 | } 131 | return string(body), resp.Header.Get(traceIDHeader), nil 132 | } 133 | 134 | func startHTTPServer(t *testing.T) (addr string, def func()) { 135 | mux := http.NewServeMux() 136 | mon := monkit.Package() 137 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 138 | ctx := r.Context() 139 | defer mon.Task()(&ctx)(nil) 140 | s := monkit.SpanFromCtx(ctx) 141 | 142 | grandParent := int64(0) 143 | parent, found := s.ParentId() 144 | 145 | var annotations []string 146 | 147 | if found { 148 | // we are interested about the parent of the parent, 149 | // created by the TraceHandler 150 | monkit.RootSpans(func(s *monkit.Span) { 151 | if s.Id() == parent { 152 | grandParent, _ = s.ParentId() 153 | } 154 | ann := s.Annotations() 155 | sort.Slice(ann, func(i, j int) bool { 156 | return ann[i].Name < ann[j].Name 157 | }) 158 | for _, a := range ann { 159 | annotations = append(annotations, fmt.Sprintf("%s=%s", a.Name, a.Value)) 160 | } 161 | }) 162 | } 163 | 164 | _, _ = fmt.Fprintf(w, "%d/%s/%v (%s)", grandParent, "hello", s.Trace().Get(present.SampledKey), strings.Join(annotations, ",")) 165 | }) 166 | 167 | listener, err := net.Listen("tcp", ":0") 168 | addr = fmt.Sprintf("http://localhost:%d", listener.Addr().(*net.TCPAddr).Port) 169 | if err != nil { 170 | t.Fatal("Couldn't start tcp listener", err) 171 | } 172 | 173 | server := &http.Server{Addr: "localhost:5050", Handler: TraceHandler(mux, monkit.ScopeNamed("server"), "k")} 174 | 175 | go func() { 176 | _ = server.Serve(listener) 177 | }() 178 | return addr, func() { 179 | _ = server.Close() 180 | _ = listener.Close() 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /http/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Storj Labs, Inc. 2 | // See LICENSE for copying information. 3 | 4 | /* 5 | Package http provides helper to propagate monkit traces via HTTP calls. 6 | */ 7 | package http 8 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package http 16 | 17 | import ( 18 | "net/http" 19 | ) 20 | 21 | // Client is an interface that matches a http.Client 22 | type Client interface { 23 | Do(req *http.Request) (*http.Response, error) 24 | } 25 | 26 | var _ http.ResponseWriter = &responseWriterObserver{} 27 | 28 | // Wrap wraps original writer + provides func to retrieve statusCode, implements http.Flusher if original writer also did it. 29 | func Wrap(w http.ResponseWriter) (http.ResponseWriter, func() int) { 30 | observer := &responseWriterObserver{ 31 | w: w, 32 | } 33 | flusher, isFlusher := w.(http.Flusher) 34 | if isFlusher { 35 | return struct { 36 | http.ResponseWriter 37 | http.Flusher 38 | }{ 39 | ResponseWriter: observer, 40 | Flusher: flusher, 41 | }, observer.StatusCode 42 | } 43 | return observer, observer.StatusCode 44 | } 45 | 46 | type responseWriterObserver struct { 47 | w http.ResponseWriter 48 | sc int 49 | } 50 | 51 | func (w *responseWriterObserver) WriteHeader(statusCode int) { 52 | w.sc = statusCode 53 | w.w.WriteHeader(statusCode) 54 | } 55 | 56 | func (w *responseWriterObserver) Write(p []byte) (n int, err error) { 57 | if w.sc == 0 { 58 | w.sc = 200 59 | } 60 | return w.w.Write(p) 61 | } 62 | 63 | func (w *responseWriterObserver) Header() http.Header { 64 | return w.w.Header() 65 | } 66 | 67 | func (w *responseWriterObserver) StatusCode() int { 68 | if w.sc == 0 { 69 | return 200 70 | } 71 | return w.sc 72 | } 73 | -------------------------------------------------------------------------------- /http/http_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Storj Labs, Inc. 2 | // See LICENSE for copying information. 3 | 4 | package http 5 | 6 | import ( 7 | "net/http" 8 | "testing" 9 | ) 10 | 11 | func TestWrapping(t *testing.T) { 12 | rw := &responseWriter{ 13 | data: []byte{}, 14 | } 15 | wrapped, statusCode := Wrap(rw) 16 | _, _ = wrapped.Write([]byte{1, 2, 3}) 17 | wrapped.WriteHeader(123) 18 | 19 | _, fok := wrapped.(http.Flusher) 20 | if fok { 21 | t.Fatalf("wrapped writer is a flusher, but the original writer is not") 22 | } 23 | 24 | if len(rw.data) != 3 { 25 | t.Fatalf("bytes are not injected (%d size)", len(rw.data)) 26 | } 27 | 28 | if statusCode() != 123 { 29 | t.Fatalf("Status code is not saved") 30 | } 31 | } 32 | 33 | func TestWrappingFlusher(t *testing.T) { 34 | rw := &responseWriterFlusher{ 35 | data: []byte{}, 36 | } 37 | wrapped, statusCode := Wrap(rw) 38 | _, _ = wrapped.Write([]byte{1, 2, 3}) 39 | wrapped.WriteHeader(123) 40 | 41 | flusher, fok := wrapped.(http.Flusher) 42 | if !fok { 43 | t.Fatalf("wrapped writer is not a flusher") 44 | } 45 | flusher.Flush() 46 | 47 | if !rw.flushed { 48 | t.Fatalf("Not flushed") 49 | } 50 | 51 | if len(rw.data) != 3 { 52 | t.Fatalf("bytes are not injected (%d size)", len(rw.data)) 53 | } 54 | 55 | if statusCode() != 123 { 56 | t.Fatalf("Status code is not saved") 57 | } 58 | } 59 | 60 | type responseWriter struct { 61 | data []byte 62 | } 63 | 64 | func (r *responseWriter) Header() http.Header { 65 | return http.Header{} 66 | } 67 | 68 | func (r *responseWriter) Write(bytes []byte) (int, error) { 69 | r.data = append(r.data, bytes...) 70 | return len(bytes), nil 71 | } 72 | 73 | func (r *responseWriter) WriteHeader(statusCode int) { 74 | 75 | } 76 | 77 | var _ http.ResponseWriter = &responseWriter{} 78 | 79 | type responseWriterFlusher struct { 80 | data []byte 81 | flushed bool 82 | } 83 | 84 | func (r *responseWriterFlusher) Flush() { 85 | r.flushed = true 86 | } 87 | 88 | func (r *responseWriterFlusher) Header() http.Header { 89 | return http.Header{} 90 | } 91 | 92 | func (r *responseWriterFlusher) Write(bytes []byte) (int, error) { 93 | r.data = append(r.data, bytes...) 94 | return len(bytes), nil 95 | } 96 | 97 | func (r *responseWriterFlusher) WriteHeader(statusCode int) { 98 | 99 | } 100 | 101 | var _ http.ResponseWriter = &responseWriterFlusher{} 102 | var _ http.Flusher = &responseWriterFlusher{} 103 | -------------------------------------------------------------------------------- /http/info.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package http 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | "strings" 21 | 22 | "github.com/spacemonkeygo/monkit/v3" 23 | "github.com/spacemonkeygo/monkit/v3/present" 24 | ) 25 | 26 | const ( 27 | // see: https://github.com/w3c/trace-context/blob/main/spec/20-http_request_header_format.md 28 | traceSampled = byte(1) 29 | traceParentHeader = "traceparent" 30 | traceStateHeader = "tracestate" 31 | 32 | // see: https://www.w3.org/TR/baggage/ 33 | baggageHeader = "baggage" 34 | 35 | // see: https://github.com/w3c/trace-context/blob/main/spec/21-http_response_header_format.md 36 | traceIDHeader = "trace-id" 37 | childIDHeader = "child-id" 38 | 39 | // orphanSampling is a special k,v which can be added to the vendor specific tracestate header. 40 | // it can turn on trace sampling on remote even without propagating the parent trace 41 | // (traceparent must not contain zero IDs) 42 | // useful when you use curl (no client side tracing), and would like to get traces from the server. 43 | orphanSampling = "sampled=true" 44 | ) 45 | 46 | // TraceInfo is a structure representing an incoming RPC request. Every field 47 | // is optional. 48 | type TraceInfo struct { 49 | TraceId *int64 50 | ParentId *int64 51 | Sampled bool 52 | Baggage map[string]string 53 | } 54 | 55 | // HeaderGetter is an interface that http.Header matches for RequestFromHeader 56 | type HeaderGetter interface { 57 | Get(string) string 58 | } 59 | 60 | // HeaderSetter is an interface that http.Header matches for TraceInfo.SetHeader 61 | type HeaderSetter interface { 62 | Set(string, string) 63 | } 64 | 65 | // TraceInfoFromHeader will create a TraceInfo object given a http.Header or 66 | // anything that matches the HeaderGetter interface. 67 | func TraceInfoFromHeader(header HeaderGetter, allowedBaggage ...string) (rv TraceInfo) { 68 | traceParent := header.Get(traceParentHeader) 69 | traceState := header.Get(traceStateHeader) 70 | baggage := header.Get(baggageHeader) 71 | 72 | if traceParent != "" { 73 | parts := strings.Split(traceParent, "-") 74 | if len(parts) != 4 { 75 | return rv 76 | } 77 | version, err := hexToUint64(parts[0]) 78 | if err != nil || version != 0 { 79 | return rv 80 | } 81 | traceID, err := hexToUint64(parts[1]) 82 | if err != nil { 83 | return rv 84 | } 85 | parentID, err := hexToUint64(parts[2]) 86 | if err != nil { 87 | return rv 88 | } 89 | flags, err := hexToUint64(parts[3]) 90 | if err != nil { 91 | return rv 92 | } 93 | 94 | bm := map[string]string{} 95 | if baggage != "" { 96 | for _, kv := range strings.Split(baggage, ",") { 97 | if key, value, ok := strings.Cut(kv, "="); ok { 98 | for _, b := range allowedBaggage { 99 | if key == b { 100 | bm[key] = value 101 | break 102 | } 103 | } 104 | } 105 | } 106 | } 107 | return TraceInfo{ 108 | TraceId: &traceID, 109 | ParentId: &parentID, 110 | Sampled: (byte(flags) & traceSampled) == traceSampled, 111 | Baggage: bm, 112 | } 113 | } 114 | 115 | // trace parent is not set, but tracing can be turned on by a traceState 116 | if strings.Contains(traceState, "sampled=true") { 117 | return TraceInfo{ 118 | Sampled: true, 119 | } 120 | } 121 | return rv 122 | } 123 | 124 | func ref(v int64) *int64 { 125 | return &v 126 | } 127 | 128 | func TraceInfoFromSpan(s *monkit.Span) TraceInfo { 129 | trace := s.Trace() 130 | 131 | sampled, _ := trace.Get(present.SampledKey).(bool) 132 | 133 | if !sampled { 134 | return TraceInfo{Sampled: sampled} 135 | } 136 | 137 | req := TraceInfo{ 138 | TraceId: ref(trace.Id()), 139 | ParentId: ref(s.Id()), 140 | Sampled: sampled, 141 | } 142 | if parentID, hasParent := s.ParentId(); hasParent { 143 | req.ParentId = ref(parentID) 144 | } 145 | return req 146 | } 147 | 148 | // SetHeader will take a TraceInfo and fill out an http.Header, or anything that 149 | // matches the HeaderSetter interface. 150 | func (r TraceInfo) SetHeader(header HeaderSetter) { 151 | sampled := byte(0) 152 | if r.Sampled { 153 | sampled = traceSampled 154 | } 155 | if r.TraceId != nil && r.ParentId != nil { 156 | header.Set(traceParentHeader, fmt.Sprintf("00-%016x-%08x-%x", *r.TraceId, *r.ParentId, int(sampled))) 157 | } else if r.Sampled { 158 | header.Set(traceStateHeader, orphanSampling) 159 | } 160 | 161 | var baggage []string 162 | if r.Baggage != nil { 163 | for k, v := range r.Baggage { 164 | baggage = append(baggage, fmt.Sprintf("%s=%s", k, v)) 165 | } 166 | header.Set(baggageHeader, strings.Join(baggage, ",")) 167 | } 168 | } 169 | 170 | // hexToUint64 reads a signed int64 that has been formatted as a hex uint64 171 | func hexToUint64(s string) (int64, error) { 172 | v, err := strconv.ParseUint(s, 16, 64) 173 | return int64(v), err 174 | } 175 | -------------------------------------------------------------------------------- /http/info_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Storj Labs, Inc. 2 | // See LICENSE for copying information. 3 | 4 | package http 5 | 6 | import ( 7 | "net/http" 8 | "testing" 9 | ) 10 | 11 | func TestSetHeader(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | info TraceInfo 15 | expectedInfo TraceInfo 16 | expectedParent string 17 | expectedState string 18 | }{ 19 | { 20 | name: "not sampled, but with trace", 21 | info: TraceInfo{ 22 | TraceId: ref(1), 23 | ParentId: ref(2), 24 | Sampled: false, 25 | }, 26 | expectedInfo: TraceInfo{ 27 | TraceId: ref(1), 28 | ParentId: ref(2), 29 | Sampled: false, 30 | }, 31 | expectedParent: "00-0000000000000001-00000002-0", 32 | expectedState: "", 33 | }, 34 | { 35 | name: "sampled", 36 | info: TraceInfo{ 37 | TraceId: ref(1), 38 | ParentId: ref(16), 39 | Sampled: true, 40 | }, 41 | expectedInfo: TraceInfo{ 42 | TraceId: ref(1), 43 | ParentId: ref(16), 44 | Sampled: true, 45 | }, 46 | expectedParent: "00-0000000000000001-00000010-1", 47 | expectedState: "", 48 | }, 49 | { 50 | name: "sampled without trace", 51 | info: TraceInfo{ 52 | TraceId: nil, 53 | ParentId: ref(16), 54 | Sampled: true, 55 | }, 56 | expectedInfo: TraceInfo{ 57 | TraceId: nil, 58 | ParentId: nil, 59 | Sampled: true, 60 | }, 61 | expectedParent: "", 62 | expectedState: "sampled=true", 63 | }, 64 | { 65 | name: "sampled with baggage", 66 | info: TraceInfo{ 67 | TraceId: ref(1), 68 | ParentId: ref(16), 69 | Sampled: true, 70 | Baggage: map[string]string{ 71 | "k": "v1", 72 | }, 73 | }, 74 | expectedInfo: TraceInfo{ 75 | TraceId: ref(1), 76 | ParentId: ref(16), 77 | Sampled: true, 78 | Baggage: map[string]string{ 79 | "k": "v1", 80 | }, 81 | }, 82 | expectedParent: "00-0000000000000001-00000010-1", 83 | expectedState: "", 84 | }, 85 | { 86 | name: "no trace, no sampled", 87 | info: TraceInfo{ 88 | TraceId: nil, 89 | ParentId: ref(16), 90 | Sampled: false, 91 | }, 92 | expectedInfo: TraceInfo{ 93 | TraceId: nil, 94 | ParentId: nil, 95 | Sampled: false, 96 | }, 97 | expectedParent: "", 98 | expectedState: "", 99 | }, 100 | } 101 | 102 | for _, tc := range tests { 103 | t.Run(tc.name, func(t *testing.T) { 104 | header := http.Header{} 105 | tc.info.SetHeader(header) 106 | if header.Get(traceParentHeader) != tc.expectedParent { 107 | t.Fatalf("%s!=%s", tc.expectedParent, header.Get(traceParentHeader)) 108 | } 109 | if header.Get(traceStateHeader) != tc.expectedState { 110 | t.Fatalf("%s!=%s", tc.expectedState, header.Get(traceParentHeader)) 111 | } 112 | rv := TraceInfoFromHeader(header, "k") 113 | checkEq(t, tc.expectedInfo.TraceId, rv.TraceId) 114 | checkEq(t, tc.expectedInfo.ParentId, rv.ParentId) 115 | if tc.expectedInfo.Sampled != rv.Sampled { 116 | t.Fatalf("%v!=%v", tc.expectedInfo.Sampled, rv.Sampled) 117 | } 118 | for k, v := range rv.Baggage { 119 | if tc.expectedInfo.Baggage[k] != v { 120 | t.Fatalf("%v!=%v", tc.expectedInfo.Baggage[k], v) 121 | } 122 | 123 | } 124 | for k, v := range tc.expectedInfo.Baggage { 125 | if rv.Baggage[k] != v { 126 | t.Fatalf("%v!=%v", rv.Baggage[k], v) 127 | } 128 | } 129 | }) 130 | 131 | } 132 | 133 | } 134 | 135 | func checkEq(t *testing.T, v1 *int64, v2 *int64) { 136 | if v1 == nil && v2 == nil { 137 | return 138 | } 139 | if v1 == nil || v2 == nil { 140 | t.Fatalf("One value is nil, other isn't") 141 | } 142 | if *v1 != *v2 { 143 | t.Fatalf("%d!=%d", v1, v2) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /http/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Storj Labs, Inc. 2 | // See LICENSE for copying information. 3 | 4 | package http 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/spacemonkeygo/monkit/v3" 11 | "github.com/spacemonkeygo/monkit/v3/present" 12 | ) 13 | 14 | // TraceHandler wraps a HTTPHandler and import trace information from header. 15 | func TraceHandler(c http.Handler, scope *monkit.Scope, allowedBaggage ...string) http.Handler { 16 | return traceHandler{ 17 | handler: c, 18 | scope: scope, 19 | allowedBaggage: allowedBaggage, 20 | } 21 | } 22 | 23 | type traceHandler struct { 24 | handler http.Handler 25 | scope *monkit.Scope 26 | 27 | // allowedBaggage defines the allowed `baggage: k=v` HTTP headers which are imported as scan annotations. 28 | allowedBaggage []string 29 | } 30 | 31 | // ServeHTTP implements http.Handler with span propagation. 32 | func (t traceHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { 33 | 34 | info := TraceInfoFromHeader(request.Header, t.allowedBaggage...) 35 | 36 | traceId := monkit.NewId() 37 | if info.TraceId != nil { 38 | traceId = *info.TraceId 39 | } 40 | 41 | trace := monkit.NewTrace(traceId) 42 | ctx := request.Context() 43 | 44 | parent := int64(0) 45 | if info.ParentId != nil { 46 | parent = *info.ParentId 47 | } 48 | 49 | if info.Sampled { 50 | trace.Set(present.SampledKey, true) 51 | } 52 | defer t.scope.Func().RemoteTrace(&ctx, parent, trace)(nil) 53 | 54 | if cb, exists := trace.Get(present.SampledCBKey).(func(*monkit.Trace)); exists { 55 | cb(trace) 56 | } 57 | 58 | s := monkit.SpanFromCtx(ctx) 59 | for k, v := range info.Baggage { 60 | s.Annotate(k, v) 61 | } 62 | s.Annotate("http.uri", request.RequestURI) 63 | 64 | wrapped, statusCode := Wrap(writer) 65 | if info.ParentId == nil && info.Sampled { 66 | writer.Header().Set(traceIDHeader, fmt.Sprintf("%x", s.Trace().Id())) 67 | writer.Header().Set(childIDHeader, fmt.Sprintf("%x", s.Id())) 68 | } 69 | t.handler.ServeHTTP(wrapped, request.WithContext(s)) 70 | 71 | s.Annotate("http.responsecode", fmt.Sprint(statusCode())) 72 | } 73 | -------------------------------------------------------------------------------- /id.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | crand "crypto/rand" 19 | "encoding/binary" 20 | "math/rand" 21 | "sync/atomic" 22 | 23 | "github.com/spacemonkeygo/monkit/v3/monotime" 24 | ) 25 | 26 | var ( 27 | idCounter uint64 28 | inc uint64 29 | ) 30 | 31 | func init() { 32 | var buf [16]byte 33 | if _, err := crand.Read(buf[:]); err == nil { 34 | idCounter = binary.BigEndian.Uint64(buf[0:8]) >> 1 35 | inc = binary.BigEndian.Uint64(buf[0:8])>>1 | 3 36 | } else { 37 | rng := rand.New(rand.NewSource(monotime.Now().UnixNano())) 38 | idCounter = uint64(rng.Int63()) 39 | inc = uint64(rng.Int63() | 3) 40 | } 41 | } 42 | 43 | // NewId returns a random integer intended for use when constructing new 44 | // traces. See NewTrace. 45 | func NewId() int64 { 46 | id := atomic.AddUint64(&idCounter, inc) 47 | return int64(id >> 1) 48 | } 49 | -------------------------------------------------------------------------------- /images/callgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemonkeygo/monkit/a1546ad16946791e15c7dcc6199dc35a0d8f98c9/images/callgraph.png -------------------------------------------------------------------------------- /images/callgraph2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemonkeygo/monkit/a1546ad16946791e15c7dcc6199dc35a0d8f98c9/images/callgraph2.png -------------------------------------------------------------------------------- /images/handlehttp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemonkeygo/monkit/a1546ad16946791e15c7dcc6199dc35a0d8f98c9/images/handlehttp.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemonkeygo/monkit/a1546ad16946791e15c7dcc6199dc35a0d8f98c9/images/logo.png -------------------------------------------------------------------------------- /images/trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemonkeygo/monkit/a1546ad16946791e15c7dcc6199dc35a0d8f98c9/images/trace.png -------------------------------------------------------------------------------- /images/trace.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 11 | 12 | main.(*VLite).HandleHTTP (330.486183ms) 13 | 14 | 15 | 16 | main.(*VLite).Proxy (130.200555ms) 17 | 18 | 19 | -------------------------------------------------------------------------------- /intdist.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // WARNING: THE NON-M4 VERSIONS OF THIS FILE ARE GENERATED BY GO GENERATE! 16 | // ONLY MAKE CHANGES TO THE M4 FILE 17 | // 18 | 19 | package monkit 20 | 21 | import ( 22 | "sort" 23 | ) 24 | 25 | // IntDist keeps statistics about values such as 26 | // low/high/recent/average/quantiles. Not threadsafe. Construct with 27 | // NewIntDist(). Fields are expected to be read from but not written to. 28 | type IntDist struct { 29 | // Low and High are the lowest and highest values observed since 30 | // construction or the last reset. 31 | Low, High int64 32 | 33 | // Recent is the last observed value. 34 | Recent int64 35 | 36 | // Count is the number of observed values since construction or the last 37 | // reset. 38 | Count int64 39 | 40 | // Sum is the sum of all the observed values since construction or the last 41 | // reset. 42 | Sum int64 43 | 44 | key SeriesKey 45 | reservoir [ReservoirSize]float32 46 | rng xorshift128 47 | sorted bool 48 | } 49 | 50 | func initIntDist(v *IntDist, key SeriesKey) { 51 | v.key = key 52 | v.rng = newXORShift128() 53 | } 54 | 55 | // NewIntDist creates a distribution of int64s. 56 | func NewIntDist(key SeriesKey) (d *IntDist) { 57 | d = &IntDist{} 58 | initIntDist(d, key) 59 | return d 60 | } 61 | 62 | // Insert adds a value to the distribution, updating appropriate values. 63 | func (d *IntDist) Insert(val int64) { 64 | if d.Count != 0 { 65 | if val < d.Low { 66 | d.Low = val 67 | } 68 | if val > d.High { 69 | d.High = val 70 | } 71 | } else { 72 | d.Low = val 73 | d.High = val 74 | } 75 | d.Recent = val 76 | d.Sum += val 77 | 78 | index := d.Count 79 | d.Count += 1 80 | 81 | if index < ReservoirSize { 82 | d.reservoir[index] = float32(val) 83 | d.sorted = false 84 | } else { 85 | window := d.Count 86 | // careful, the capitalization of Window is important 87 | if Window > 0 && window > Window { 88 | window = Window 89 | } 90 | // fast, but kind of biased. probably okay 91 | j := d.rng.Uint64() % uint64(window) 92 | if j < ReservoirSize { 93 | d.reservoir[int(j)] = float32(val) 94 | d.sorted = false 95 | } 96 | } 97 | } 98 | 99 | // FullAverage calculates and returns the average of all inserted values. 100 | func (d *IntDist) FullAverage() int64 { 101 | if d.Count > 0 { 102 | return d.Sum / int64(d.Count) 103 | } 104 | return 0 105 | } 106 | 107 | // ReservoirAverage calculates the average of the current reservoir. 108 | func (d *IntDist) ReservoirAverage() int64 { 109 | amount := ReservoirSize 110 | if d.Count < int64(amount) { 111 | amount = int(d.Count) 112 | } 113 | if amount <= 0 { 114 | return 0 115 | } 116 | var sum float32 117 | for i := 0; i < amount; i++ { 118 | sum += d.reservoir[i] 119 | } 120 | return int64(sum / float32(amount)) 121 | } 122 | 123 | // Query will return the approximate value at the given quantile from the 124 | // reservoir, where 0 <= quantile <= 1. 125 | func (d *IntDist) Query(quantile float64) int64 { 126 | rlen := int(ReservoirSize) 127 | if int64(rlen) > d.Count { 128 | rlen = int(d.Count) 129 | } 130 | 131 | if rlen < 2 { 132 | return int64(d.reservoir[0]) 133 | } 134 | 135 | reservoir := d.reservoir[:rlen] 136 | if !d.sorted { 137 | sort.Sort(float32Slice(reservoir)) 138 | d.sorted = true 139 | } 140 | 141 | if quantile <= 0 { 142 | return int64(reservoir[0]) 143 | } 144 | if quantile >= 1 { 145 | return int64(reservoir[rlen-1]) 146 | } 147 | 148 | idx_float := quantile * float64(rlen-1) 149 | idx := int(idx_float) 150 | 151 | diff := idx_float - float64(idx) 152 | prior := float64(reservoir[idx]) 153 | return int64(prior + diff*(float64(reservoir[idx+1])-prior)) 154 | } 155 | 156 | // Copy returns a full copy of the entire distribution. 157 | func (d *IntDist) Copy() *IntDist { 158 | cp := *d 159 | cp.rng = newXORShift128() 160 | return &cp 161 | } 162 | 163 | func (d *IntDist) Reset() { 164 | d.Low, d.High, d.Recent, d.Count, d.Sum = 0, 0, 0, 0, 0 165 | // resetting count will reset the quantile reservoir 166 | } 167 | 168 | func (d *IntDist) Stats(cb func(key SeriesKey, field string, val float64)) { 169 | count := d.Count 170 | cb(d.key, "count", float64(count)) 171 | if count > 0 { 172 | cb(d.key, "sum", d.toFloat64(d.Sum)) 173 | cb(d.key, "min", d.toFloat64(d.Low)) 174 | cb(d.key, "max", d.toFloat64(d.High)) 175 | cb(d.key, "rmin", d.toFloat64(d.Query(0))) 176 | cb(d.key, "ravg", d.toFloat64(d.ReservoirAverage())) 177 | cb(d.key, "r10", d.toFloat64(d.Query(.1))) 178 | cb(d.key, "r50", d.toFloat64(d.Query(.5))) 179 | cb(d.key, "r90", d.toFloat64(d.Query(.9))) 180 | cb(d.key, "r99", d.toFloat64(d.Query(.99))) 181 | cb(d.key, "rmax", d.toFloat64(d.Query(1))) 182 | cb(d.key, "recent", d.toFloat64(d.Recent)) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /internal/testpkg1/main.go: -------------------------------------------------------------------------------- 1 | package testpkg1 2 | 3 | import ( 4 | "context" 5 | 6 | monkit "github.com/spacemonkeygo/monkit/v3" 7 | ) 8 | 9 | var ( 10 | mon = monkit.Package() 11 | ) 12 | 13 | func TestFunc(ctx context.Context, e error) (err error) { 14 | defer mon.Task()(&ctx)(&err) 15 | mon.Event("test_event") 16 | return e 17 | } 18 | -------------------------------------------------------------------------------- /internal/tests/caller_test.go: -------------------------------------------------------------------------------- 1 | package monkit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/spacemonkeygo/monkit/v3" 9 | "github.com/spacemonkeygo/monkit/v3/internal/testpkg1" 10 | ) 11 | 12 | func TestCallers(t *testing.T) { 13 | ctx := context.Background() 14 | testpkg1.TestFunc(ctx, nil) 15 | testpkg1.TestFunc(ctx, fmt.Errorf("new error")) 16 | stats := monkit.Collect(monkit.Default) 17 | 18 | assertEqual(t, 19 | stats["function,name=TestFunc,scope=github.com/spacemonkeygo/monkit/v3/internal/testpkg1 total"], 2) 20 | assertEqual(t, 21 | stats["function,name=TestFunc,scope=github.com/spacemonkeygo/monkit/v3/internal/testpkg1 successes"], 1) 22 | assertEqual(t, 23 | stats["function,name=TestFunc,scope=github.com/spacemonkeygo/monkit/v3/internal/testpkg1 errors"], 1) 24 | assertEqual(t, 25 | stats["test_event,scope=github.com/spacemonkeygo/monkit/v3/internal/testpkg1 total"], 2) 26 | } 27 | 28 | func assertEqual(t *testing.T, actual, expected float64) { 29 | t.Helper() 30 | if actual != expected { 31 | t.Fatalf("got %v, expected %v", actual, expected) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /meter.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "sync" 19 | "time" 20 | 21 | "github.com/spacemonkeygo/monkit/v3/monotime" 22 | ) 23 | 24 | const ( 25 | ticksToKeep = 24 26 | timePerTick = 10 * time.Minute 27 | ) 28 | 29 | var ( 30 | defaultTicker = ticker{} 31 | ) 32 | 33 | type meterBucket struct { 34 | count int64 35 | start time.Time 36 | } 37 | 38 | // Meter keeps track of events and their rates over time. 39 | // Implements the StatSource interface. You should construct using NewMeter, 40 | // though expected usage is like: 41 | // 42 | // var ( 43 | // mon = monkit.Package() 44 | // meter = mon.Meter("meter") 45 | // ) 46 | // 47 | // func MyFunc() { 48 | // ... 49 | // meter.Mark(4) // 4 things happened 50 | // ... 51 | // } 52 | type Meter struct { 53 | mtx sync.Mutex 54 | total int64 55 | slices [ticksToKeep]meterBucket 56 | key SeriesKey 57 | } 58 | 59 | // NewMeter constructs a Meter 60 | func NewMeter(key SeriesKey) *Meter { 61 | rv := &Meter{key: key} 62 | now := monotime.Now() 63 | for i := 0; i < ticksToKeep; i++ { 64 | rv.slices[i].start = now 65 | } 66 | defaultTicker.register(rv) 67 | return rv 68 | } 69 | 70 | // Reset resets all internal state. 71 | // 72 | // Useful when monitoring a counter that has overflowed. 73 | func (e *Meter) Reset(new_total int64) { 74 | e.mtx.Lock() 75 | e.total = new_total 76 | now := monotime.Now() 77 | for i := range e.slices { 78 | e.slices[i].count = 0 79 | e.slices[i].start = now 80 | } 81 | e.mtx.Unlock() 82 | } 83 | 84 | // SetTotal sets the initial total count of the meter. 85 | func (e *Meter) SetTotal(total int64) { 86 | e.mtx.Lock() 87 | e.total = total 88 | e.mtx.Unlock() 89 | } 90 | 91 | // Mark marks amount events occurring in the current time window. 92 | func (e *Meter) Mark(amount int) { 93 | e.mtx.Lock() 94 | e.slices[ticksToKeep-1].count += int64(amount) 95 | e.mtx.Unlock() 96 | } 97 | 98 | // Mark64 marks amount events occurring in the current time window (int64 version). 99 | func (e *Meter) Mark64(amount int64) { 100 | e.mtx.Lock() 101 | e.slices[ticksToKeep-1].count += amount 102 | e.mtx.Unlock() 103 | } 104 | 105 | func (e *Meter) tick(now time.Time) { 106 | e.mtx.Lock() 107 | // only advance meter buckets if something happened. otherwise 108 | // rare events will always just have zero rates. 109 | if e.slices[ticksToKeep-1].count != 0 { 110 | e.total += e.slices[0].count 111 | copy(e.slices[:], e.slices[1:]) 112 | e.slices[ticksToKeep-1] = meterBucket{count: 0, start: now} 113 | } 114 | e.mtx.Unlock() 115 | } 116 | 117 | func (e *Meter) stats(now time.Time) (rate float64, total int64) { 118 | current := int64(0) 119 | e.mtx.Lock() 120 | start := e.slices[0].start 121 | for i := 0; i < ticksToKeep; i++ { 122 | current += e.slices[i].count 123 | } 124 | total = e.total 125 | e.mtx.Unlock() 126 | total += current 127 | duration := now.Sub(start).Seconds() 128 | if duration > 0 { 129 | rate = float64(current) / duration 130 | } else { 131 | rate = 0 132 | } 133 | return rate, total 134 | } 135 | 136 | // Rate returns the rate over the internal sliding window 137 | func (e *Meter) Rate() float64 { 138 | rate, _ := e.stats(monotime.Now()) 139 | return rate 140 | } 141 | 142 | // Total returns the total over the internal sliding window 143 | func (e *Meter) Total() float64 { 144 | _, total := e.stats(monotime.Now()) 145 | return float64(total) 146 | } 147 | 148 | // Stats implements the StatSource interface 149 | func (e *Meter) Stats(cb func(key SeriesKey, field string, val float64)) { 150 | rate, total := e.stats(monotime.Now()) 151 | cb(e.key, "rate", rate) 152 | cb(e.key, "total", float64(total)) 153 | } 154 | 155 | // DiffMeter is a StatSource that shows the difference between 156 | // the rates of two meters. Expected usage like: 157 | // 158 | // var ( 159 | // mon = monkit.Package() 160 | // herps = mon.Meter("herps") 161 | // derps = mon.Meter("derps") 162 | // herpToDerp = mon.DiffMeter("herp_to_derp", herps, derps) 163 | // ) 164 | type DiffMeter struct { 165 | meter1, meter2 *Meter 166 | key SeriesKey 167 | } 168 | 169 | // Constructs a DiffMeter. 170 | func NewDiffMeter(key SeriesKey, meter1, meter2 *Meter) *DiffMeter { 171 | return &DiffMeter{key: key, meter1: meter1, meter2: meter2} 172 | } 173 | 174 | // Stats implements the StatSource interface 175 | func (m *DiffMeter) Stats(cb func(key SeriesKey, field string, val float64)) { 176 | now := monotime.Now() 177 | rate1, total1 := m.meter1.stats(now) 178 | rate2, total2 := m.meter2.stats(now) 179 | cb(m.key, "rate", rate1-rate2) 180 | cb(m.key, "total", float64(total1-total2)) 181 | } 182 | 183 | type ticker struct { 184 | mtx sync.Mutex 185 | started bool 186 | meters []*Meter 187 | } 188 | 189 | func (t *ticker) register(m *Meter) { 190 | t.mtx.Lock() 191 | if !t.started { 192 | t.started = true 193 | go t.run() 194 | } 195 | t.meters = append(t.meters, m) 196 | t.mtx.Unlock() 197 | } 198 | 199 | func (t *ticker) run() { 200 | for { 201 | time.Sleep(timePerTick) 202 | t.mtx.Lock() 203 | meters := t.meters // this is safe since we only use append 204 | t.mtx.Unlock() 205 | now := monotime.Now() 206 | for _, m := range meters { 207 | m.tick(now) 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /monotime/monotime.go: -------------------------------------------------------------------------------- 1 | package monotime 2 | 3 | import "time" 4 | 5 | var initTime = time.Now() 6 | 7 | func Now() time.Time { return initTime.Add(elapsed()) } 8 | -------------------------------------------------------------------------------- /monotime/monotime_fallback.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package monotime 5 | 6 | import "time" 7 | 8 | func elapsed() time.Duration { return time.Since(initTime) } 9 | -------------------------------------------------------------------------------- /monotime/monotime_test.go: -------------------------------------------------------------------------------- 1 | package monotime 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestMonotime(t *testing.T) { 9 | const sleep = time.Second 10 | start := Now() 11 | time.Sleep(sleep) 12 | finish := Now() 13 | 14 | delta := finish.Sub(start) - sleep 15 | if delta < 0 { 16 | delta = -delta 17 | } 18 | if delta > time.Second/4 { 19 | t.Errorf("delta too large %v", delta) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /monotime/monotime_windows.go: -------------------------------------------------------------------------------- 1 | package monotime 2 | 3 | import ( 4 | "syscall" 5 | "time" 6 | "unsafe" 7 | ) 8 | 9 | var ( 10 | modkernel32 = syscall.NewLazyDLL("kernel32.dll") 11 | queryPerformanceFrequencyProc = modkernel32.NewProc("QueryPerformanceFrequency") 12 | queryPerformanceCounterProc = modkernel32.NewProc("QueryPerformanceCounter") 13 | 14 | qpcFrequency = queryPerformanceFrequency() 15 | qpcBase = queryPerformanceCounter() 16 | ) 17 | 18 | func elapsed() time.Duration { 19 | elapsed := queryPerformanceCounter() - qpcBase 20 | return time.Duration(elapsed) * time.Second / (time.Duration(qpcFrequency) * time.Nanosecond) 21 | } 22 | 23 | func queryPerformanceCounter() int64 { 24 | var count int64 25 | syscall.SyscallN(queryPerformanceCounterProc.Addr(), uintptr(unsafe.Pointer(&count))) 26 | return count 27 | } 28 | 29 | func queryPerformanceFrequency() int64 { 30 | var freq int64 31 | syscall.SyscallN(queryPerformanceFrequencyProc.Addr(), uintptr(unsafe.Pointer(&freq))) 32 | return freq 33 | } 34 | -------------------------------------------------------------------------------- /present/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package present // import "github.com/spacemonkeygo/monkit/v3/present" 16 | -------------------------------------------------------------------------------- /present/dot.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package present 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | func escapeDotLabel(format string, args ...interface{}) string { 22 | val := fmt.Sprintf(format, args...) 23 | var rv []byte 24 | for _, b := range []byte(val) { 25 | switch { 26 | case 'A' <= b && b <= 'Z', 'a' <= b && b <= 'z', '0' <= b && b <= '9', 128 <= b, ' ' == b: 27 | rv = append(rv, b) 28 | case b == '\n': 29 | rv = append(rv, []byte(`\l`)...) 30 | default: 31 | rv = append(rv, []byte(fmt.Sprintf("&#%d;", int(b)))...) 32 | } 33 | } 34 | return string(rv) 35 | } 36 | -------------------------------------------------------------------------------- /present/errs.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package present 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | ) 21 | 22 | type errorKind string 23 | 24 | const ( 25 | errBadRequest errorKind = "Bad Request" 26 | errNotFound errorKind = "Not Found" 27 | ) 28 | 29 | var statusCodes = map[errorKind]int{ 30 | errBadRequest: http.StatusBadRequest, 31 | errNotFound: http.StatusNotFound, 32 | } 33 | 34 | func getStatusCode(err error, def int) int { 35 | if err, ok := err.(errorT); ok { 36 | if code, ok := statusCodes[err.kind]; ok { 37 | return code 38 | } 39 | } 40 | return def 41 | } 42 | 43 | func (e errorKind) New(format string, args ...interface{}) error { 44 | return errorT{ 45 | kind: e, 46 | message: fmt.Sprintf(format, args...), 47 | } 48 | } 49 | 50 | type errorT struct { 51 | kind errorKind 52 | message string 53 | } 54 | 55 | func (e errorT) Error() string { 56 | return fmt.Sprintf("%s: %s", string(e.kind), e.message) 57 | } 58 | -------------------------------------------------------------------------------- /present/funcs.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package present 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | 22 | "github.com/spacemonkeygo/monkit/v3" 23 | ) 24 | 25 | func formatDist(data *monkit.DurationDist, indent string) (result string) { 26 | for _, q := range monkit.ObservedQuantiles { 27 | result += fmt.Sprintf("%s%.02f: %s\n", indent, q, data.Query(q)) 28 | } 29 | result += fmt.Sprintf("%savg: %s\n", indent, data.FullAverage()) 30 | result += fmt.Sprintf("%sravg: %s\n", indent, data.ReservoirAverage()) 31 | result += fmt.Sprintf("%srecent: %s\n", indent, data.Recent) 32 | result += fmt.Sprintf("%ssum: %s\n", indent, data.Sum) 33 | return result 34 | } 35 | 36 | // FuncsDot finds all of the Funcs known by Registry r and writes information 37 | // about them in the dot graphics file format to w. 38 | func FuncsDot(r *monkit.Registry, w io.Writer) (err error) { 39 | _, err = fmt.Fprintf(w, "digraph G {\n node [shape=box];\n") 40 | if err != nil { 41 | return err 42 | } 43 | r.Funcs(func(f *monkit.Func) { 44 | if err != nil { 45 | return 46 | } 47 | success := f.Success() 48 | panics := f.Panics() 49 | 50 | var err_out bytes.Buffer 51 | total_errors := int64(0) 52 | for errname, count := range f.Errors() { 53 | _, err = fmt.Fprint(&err_out, escapeDotLabel("error %s: %d\n", errname, 54 | count)) 55 | if err != nil { 56 | return 57 | } 58 | total_errors += count 59 | } 60 | 61 | _, err = fmt.Fprintf(w, " f%d [label=\"%s", f.Id(), 62 | escapeDotLabel("%s\ncurrent: %d, highwater: %d, success: %d, "+ 63 | "errors: %d, panics: %d\n", f.FullName(), f.Current(), f.Highwater(), 64 | success, total_errors, panics)) 65 | if err != nil { 66 | return 67 | } 68 | 69 | _, err = err_out.WriteTo(w) 70 | if err != nil { 71 | return 72 | } 73 | 74 | if success > 0 { 75 | _, err = fmt.Fprint(w, escapeDotLabel( 76 | "success times:\n%s", formatDist(f.SuccessTimes(), " "))) 77 | if err != nil { 78 | return 79 | } 80 | } 81 | 82 | if total_errors+panics > 0 { 83 | _, err = fmt.Fprint(w, escapeDotLabel( 84 | "failure times:\n%s", formatDist(f.FailureTimes(), " "))) 85 | if err != nil { 86 | return 87 | } 88 | } 89 | 90 | _, err = fmt.Fprint(w, "\"];\n") 91 | if err != nil { 92 | return 93 | } 94 | 95 | f.Parents(func(parent *monkit.Func) { 96 | if err != nil { 97 | return 98 | } 99 | if parent != nil { 100 | _, err = fmt.Fprintf(w, " f%d -> f%d;\n", parent.Id(), f.Id()) 101 | if err != nil { 102 | return 103 | } 104 | } else { 105 | _, err = fmt.Fprintf(w, " r%d [label=\"entry\"];\n r%d -> f%d;\n", 106 | f.Id(), f.Id(), f.Id()) 107 | if err != nil { 108 | return 109 | } 110 | } 111 | }) 112 | }) 113 | if err != nil { 114 | return err 115 | } 116 | _, err = fmt.Fprintf(w, "}\n") 117 | return err 118 | } 119 | 120 | // FuncsText finds all of the Funcs known by Registry r and writes information 121 | // about them in a plain text format to w. 122 | func FuncsText(r *monkit.Registry, w io.Writer) (err error) { 123 | r.Funcs(func(f *monkit.Func) { 124 | if err != nil { 125 | return 126 | } 127 | _, err = fmt.Fprintf(w, "[%d] %s\n parents: ", f.Id(), f.FullName()) 128 | if err != nil { 129 | return 130 | } 131 | printed := false 132 | f.Parents(func(parent *monkit.Func) { 133 | if err != nil { 134 | return 135 | } 136 | if printed { 137 | _, err = fmt.Fprint(w, ", ") 138 | if err != nil { 139 | return 140 | } 141 | } else { 142 | printed = true 143 | } 144 | if parent != nil { 145 | _, err = fmt.Fprintf(w, "%d", parent.Id()) 146 | if err != nil { 147 | return 148 | } 149 | } else { 150 | _, err = fmt.Fprintf(w, "entry") 151 | if err != nil { 152 | return 153 | } 154 | } 155 | }) 156 | var err_out bytes.Buffer 157 | total_errors := int64(0) 158 | for errname, count := range f.Errors() { 159 | _, err = fmt.Fprintf(&err_out, " error %s: %d\n", errname, count) 160 | if err != nil { 161 | return 162 | } 163 | total_errors += count 164 | } 165 | _, err = fmt.Fprintf(w, 166 | "\n current: %d, highwater: %d, success: %d, errors: %d, panics: %d\n", 167 | f.Current(), f.Highwater(), f.Success(), total_errors, f.Panics()) 168 | if err != nil { 169 | return 170 | } 171 | _, err = err_out.WriteTo(w) 172 | if err != nil { 173 | return 174 | } 175 | _, err = fmt.Fprintf(w, " success times:\n%s failure times:\n%s\n", 176 | formatDist(f.SuccessTimes(), " "), 177 | formatDist(f.FailureTimes(), " ")) 178 | }) 179 | return err 180 | } 181 | 182 | // FuncsJSON finds all of the Funcs known by Registry r and writes information 183 | // about them in the JSON format to w. 184 | func FuncsJSON(r *monkit.Registry, w io.Writer) (err error) { 185 | lw := newListWriter(w) 186 | r.Funcs(func(f *monkit.Func) { 187 | lw.elem(formatFunc(f)) 188 | }) 189 | return lw.done() 190 | } 191 | -------------------------------------------------------------------------------- /present/http.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package present 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/spacemonkeygo/monkit/v3" 21 | ) 22 | 23 | type handler struct { 24 | Registry *monkit.Registry 25 | } 26 | 27 | // HTTP makes an http.Handler out of a Registry. It serves paths using this 28 | // package's FromRequest request router. Usually HTTP is called with the 29 | // Default registry. 30 | func HTTP(r *monkit.Registry) http.Handler { 31 | return handler{Registry: r} 32 | } 33 | 34 | func (h handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 35 | p, contentType, err := FromRequest(h.Registry, req.URL.Path, req.URL.Query()) 36 | if err != nil { 37 | http.Error(w, err.Error(), getStatusCode(err, 500)) 38 | return 39 | } 40 | w.Header().Set("Content-Type", contentType) 41 | p(w) 42 | } 43 | -------------------------------------------------------------------------------- /present/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package present 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io" 21 | "time" 22 | 23 | "github.com/spacemonkeygo/monkit/v3" 24 | "github.com/spacemonkeygo/monkit/v3/collect" 25 | ) 26 | 27 | func formatSpan(s *monkit.Span) interface{} { 28 | js := struct { 29 | Id int64 `json:"id"` 30 | ParentId *int64 `json:"parent_id,omitempty"` 31 | Func struct { 32 | Package string `json:"package"` 33 | Name string `json:"name"` 34 | } `json:"func"` 35 | Trace struct { 36 | Id int64 `json:"id"` 37 | } `json:"trace"` 38 | Start int64 `json:"start"` 39 | Elapsed int64 `json:"elapsed"` 40 | Orphaned bool `json:"orphaned"` 41 | Args []string `json:"args"` 42 | Annotations [][]string `json:"annotations"` 43 | }{} 44 | 45 | js.Id = s.Id() 46 | if parent_id, ok := s.ParentId(); ok { 47 | js.ParentId = &parent_id 48 | } 49 | js.Func.Package = s.Func().Scope().Name() 50 | js.Func.Name = s.Func().ShortName() 51 | js.Trace.Id = s.Trace().Id() 52 | js.Start = s.Start().UnixNano() 53 | js.Elapsed = time.Since(s.Start()).Nanoseconds() 54 | js.Orphaned = s.Orphaned() 55 | js.Args = make([]string, 0, len(s.Args())) 56 | for _, arg := range s.Args() { 57 | js.Args = append(js.Args, fmt.Sprintf("%#v", arg)) 58 | } 59 | js.Annotations = make([][]string, 0, len(s.Annotations())) 60 | for _, annotation := range s.Annotations() { 61 | js.Annotations = append(js.Annotations, 62 | []string{annotation.Name, annotation.Value}) 63 | } 64 | return js 65 | } 66 | 67 | func formatFinishedSpan(s *collect.FinishedSpan) interface{} { 68 | js := struct { 69 | Id int64 `json:"id"` 70 | ParentId *int64 `json:"parent_id,omitempty"` 71 | Func struct { 72 | Package string `json:"package"` 73 | Name string `json:"name"` 74 | } `json:"func"` 75 | Trace struct { 76 | Id int64 `json:"id"` 77 | } `json:"trace"` 78 | Start int64 `json:"start"` 79 | Finish int64 `json:"finish"` 80 | Orphaned bool `json:"orphaned"` 81 | Err string `json:"err"` 82 | Panicked bool `json:"panicked"` 83 | Args []string `json:"args"` 84 | Annotations [][]string `json:"annotations"` 85 | }{} 86 | js.Id = s.Span.Id() 87 | if parent_id, ok := s.Span.ParentId(); ok { 88 | js.ParentId = &parent_id 89 | } 90 | js.Func.Package = s.Span.Func().Scope().Name() 91 | js.Func.Name = s.Span.Func().ShortName() 92 | js.Trace.Id = s.Span.Trace().Id() 93 | js.Start = s.Span.Start().UnixNano() 94 | js.Finish = s.Finish.UnixNano() 95 | js.Orphaned = s.Span.Orphaned() 96 | if s.Err != nil { 97 | errstr := s.Err.Error() 98 | js.Err = errstr 99 | } 100 | js.Panicked = s.Panicked 101 | js.Args = make([]string, 0, len(s.Span.Args())) 102 | for _, arg := range s.Span.Args() { 103 | js.Args = append(js.Args, fmt.Sprintf("%#v", arg)) 104 | } 105 | js.Annotations = make([][]string, 0, len(s.Span.Annotations())) 106 | for _, annotation := range s.Span.Annotations() { 107 | js.Annotations = append(js.Annotations, 108 | []string{annotation.Name, annotation.Value}) 109 | } 110 | return js 111 | } 112 | 113 | type durationStats struct { 114 | Average time.Duration `json:"average"` 115 | ReservoirAverage time.Duration `json:"reservoir_average"` 116 | FullAverage time.Duration `json:"full_average"` 117 | High time.Duration `json:"max"` 118 | Low time.Duration `json:"min"` 119 | Recent time.Duration `json:"recent"` 120 | Quantiles map[string]time.Duration `json:"quantiles"` 121 | } 122 | 123 | func formatDuration(d *monkit.DurationDist, out *durationStats) { 124 | out.Average = d.FullAverage() 125 | out.FullAverage = d.FullAverage() 126 | out.ReservoirAverage = d.ReservoirAverage() 127 | out.High = d.High 128 | out.Low = d.Low 129 | out.Recent = d.Recent 130 | out.Quantiles = make(map[string]time.Duration, 131 | len(monkit.ObservedQuantiles)) 132 | for _, quantile := range monkit.ObservedQuantiles { 133 | name := fmt.Sprintf("%.02f", quantile) 134 | out.Quantiles[name] = d.Query(quantile) 135 | } 136 | } 137 | 138 | func formatFunc(f *monkit.Func) interface{} { 139 | js := struct { 140 | Id int64 `json:"id"` 141 | ParentIds []int64 `json:"parent_ids"` 142 | Package string `json:"package"` 143 | Name string `json:"name"` 144 | Current int64 `json:"current"` 145 | Highwater int64 `json:"highwater"` 146 | Success int64 `json:"success"` 147 | Panics int64 `json:"panics"` 148 | Entry bool `json:"entry"` 149 | Errors map[string]int64 `json:"errors"` 150 | SuccessTimes durationStats `json:"success_times"` 151 | FailureTimes durationStats `json:"failure_times"` 152 | }{} 153 | 154 | js.Id = f.Id() 155 | f.Parents(func(parent *monkit.Func) { 156 | if parent == nil { 157 | js.Entry = true 158 | } else { 159 | js.ParentIds = append(js.ParentIds, parent.Id()) 160 | } 161 | }) 162 | js.Package = f.Scope().Name() 163 | js.Name = f.ShortName() 164 | js.Current = f.Current() 165 | js.Highwater = f.Highwater() 166 | js.Success = f.Success() 167 | js.Panics = f.Panics() 168 | js.Errors = f.Errors() 169 | formatDuration(f.SuccessTimes(), &js.SuccessTimes) 170 | formatDuration(f.FailureTimes(), &js.FailureTimes) 171 | return js 172 | } 173 | 174 | type listWriter struct { 175 | w io.Writer 176 | err error 177 | sep string 178 | } 179 | 180 | func newListWriter(w io.Writer) (rv *listWriter) { 181 | rv = &listWriter{ 182 | w: w, 183 | sep: "\n"} 184 | _, rv.err = fmt.Fprint(w, "[") 185 | return rv 186 | } 187 | 188 | func (l *listWriter) elem(elem interface{}) { 189 | if l.err != nil { 190 | return 191 | } 192 | var data []byte 193 | data, l.err = json.Marshal(elem) 194 | if l.err != nil { 195 | return 196 | } 197 | _, l.err = fmt.Fprintf(l.w, "%s %s", l.sep, data) 198 | l.sep = ",\n" 199 | } 200 | 201 | func (l *listWriter) done() error { 202 | if l.err != nil { 203 | return l.err 204 | } 205 | _, err := fmt.Fprint(l.w, "]\n") 206 | return err 207 | } 208 | -------------------------------------------------------------------------------- /present/spans.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package present 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "strings" 21 | 22 | "github.com/spacemonkeygo/monkit/v3" 23 | ) 24 | 25 | func outputDotSpan(w io.Writer, s *monkit.Span) error { 26 | orphaned := "" 27 | if s.Orphaned() { 28 | orphaned = "orphaned\n" 29 | } 30 | _, err := fmt.Fprintf(w, 31 | " f%d [label=\"%s", 32 | s.Id(), escapeDotLabel("%s(%s)\nelapsed: %s\n%s", 33 | s.Func().FullName(), strings.Join(s.Args(), ", "), s.Duration(), 34 | orphaned)) 35 | if err != nil { 36 | return err 37 | } 38 | for _, annotation := range s.Annotations() { 39 | _, err = fmt.Fprint(w, escapeDotLabel("%s: %s\n", 40 | annotation.Name, annotation.Value)) 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | _, err = fmt.Fprint(w, "\"];\n") 46 | if err != nil { 47 | return err 48 | } 49 | s.Children(func(child *monkit.Span) { 50 | if err != nil { 51 | return 52 | } 53 | err = outputDotSpan(w, child) 54 | if err != nil { 55 | return 56 | } 57 | _, err = fmt.Fprintf(w, " f%d -> f%d;\n", s.Id(), child.Id()) 58 | if err != nil { 59 | return 60 | } 61 | }) 62 | return err 63 | } 64 | 65 | // SpansDot finds all of the current Spans known by Registry r and writes 66 | // information about them in the dot graphics file format to w. 67 | func SpansDot(r *monkit.Registry, w io.Writer) error { 68 | _, err := fmt.Fprintf(w, "digraph G {\n node [shape=box];\n") 69 | if err != nil { 70 | return err 71 | } 72 | r.RootSpans(func(s *monkit.Span) { 73 | if err != nil { 74 | return 75 | } 76 | err = outputDotSpan(w, s) 77 | }) 78 | if err != nil { 79 | return err 80 | } 81 | _, err = fmt.Fprintf(w, "}\n") 82 | return err 83 | } 84 | 85 | func outputTextSpan(w io.Writer, s *monkit.Span, indent string) (err error) { 86 | orphaned := "" 87 | if s.Orphaned() { 88 | orphaned = ", orphaned" 89 | } 90 | _, err = fmt.Fprintf(w, "%s[%d,%d] %s(%s) (elapsed: %s%s)\n", 91 | indent, s.Id(), s.Trace().Id(), s.Func().FullName(), strings.Join(s.Args(), ", "), 92 | s.Duration(), orphaned) 93 | if err != nil { 94 | return err 95 | } 96 | for _, annotation := range s.Annotations() { 97 | _, err = fmt.Fprintf(w, "%s %s: %s\n", indent, 98 | annotation.Name, annotation.Value) 99 | if err != nil { 100 | return err 101 | } 102 | } 103 | s.Children(func(s *monkit.Span) { 104 | if err != nil { 105 | return 106 | } 107 | err = outputTextSpan(w, s, indent+" ") 108 | }) 109 | return err 110 | } 111 | 112 | // SpansText finds all of the current Spans known by Registry r and writes 113 | // information about them in a plain text format to w. 114 | func SpansText(r *monkit.Registry, w io.Writer) (err error) { 115 | r.RootSpans(func(s *monkit.Span) { 116 | if err != nil { 117 | return 118 | } 119 | err = outputTextSpan(w, s, "") 120 | if err != nil { 121 | return 122 | } 123 | _, err = fmt.Fprintf(w, "\n") 124 | }) 125 | return err 126 | } 127 | 128 | // SpansJSON finds all of the current Spans known by Registry r and writes 129 | // information about them in the JSON format to w. 130 | func SpansJSON(r *monkit.Registry, w io.Writer) (err error) { 131 | lw := newListWriter(w) 132 | r.AllSpans(func(s *monkit.Span) { 133 | lw.elem(formatSpan(s)) 134 | }) 135 | return lw.done() 136 | } 137 | -------------------------------------------------------------------------------- /present/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package present 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | 21 | "github.com/spacemonkeygo/monkit/v3" 22 | ) 23 | 24 | // StatsOld is deprecated. 25 | func StatsOld(r *monkit.Registry, w io.Writer) error { return StatsText(r, w) } 26 | 27 | // StatsText writes all of the name/value statistics pairs the Registry knows 28 | // to w in a text format. 29 | func StatsText(r *monkit.Registry, w io.Writer) (err error) { 30 | r.Stats(func(key monkit.SeriesKey, field string, val float64) { 31 | if err != nil { 32 | return 33 | } 34 | _, err = fmt.Fprintf(w, "%s=%f\n", key.WithField(field), val) 35 | }) 36 | return err 37 | } 38 | 39 | // StatsJSON writes all of the name/value statistics pairs the Registry knows 40 | // to w in a JSON format. 41 | func StatsJSON(r *monkit.Registry, w io.Writer) (err error) { 42 | lw := newListWriter(w) 43 | r.Stats(func(key monkit.SeriesKey, field string, val float64) { 44 | lw.elem([]interface{}{key.Measurement, key.Tags.All(), field, val}) 45 | }) 46 | return lw.done() 47 | } 48 | -------------------------------------------------------------------------------- /present/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Storj Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package present 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "sync" 21 | "time" 22 | ) 23 | 24 | const keepAliveInterval = 30 * time.Second 25 | 26 | // keepAlive takes a ctx and a ping method. It returns a context rctx derived 27 | // from ctx and a stop method. The derived rctx is canceled if ping returns 28 | // a non-nil error. In the background after return, keepAlive will call ping 29 | // with ctx every keepAliveInterval until: 30 | // 1. ping returns an error, at which point rctx is canceled. 31 | // 2. stop is called. in this case rctx is left alone. 32 | // 3. ctx is canceled. rctx is also canceled as a consequence. 33 | // 34 | // stop is a no-op if the keepAlive loop has already been stopped. stop returns 35 | // the first error that ping returned, if ping returned an error before stop 36 | // was called. 37 | func keepAlive(ctx context.Context, ping func(context.Context) error) ( 38 | rctx context.Context, stop func() error) { 39 | 40 | ping = catchPanics(ping) 41 | 42 | rctx, cancel := context.WithCancel(ctx) 43 | done := make(chan bool) 44 | var once sync.Once 45 | var mu sync.Mutex 46 | var pingErr error 47 | var stopped bool 48 | 49 | ticker := time.NewTicker(keepAliveInterval) 50 | go func() { 51 | defer ticker.Stop() 52 | 53 | for { 54 | select { 55 | case <-done: 56 | return 57 | case <-ctx.Done(): 58 | return 59 | case <-ticker.C: 60 | mu.Lock() 61 | if stopped || pingErr != nil { 62 | mu.Unlock() 63 | return 64 | } 65 | if err := ping(ctx); err != nil { 66 | pingErr = err 67 | mu.Unlock() 68 | cancel() 69 | return 70 | } 71 | mu.Unlock() 72 | } 73 | } 74 | }() 75 | 76 | return rctx, func() error { 77 | once.Do(func() { close(done) }) 78 | // this mutex serves two purposes: 79 | // 1) it protects pingErr but 80 | // 2) it makes sure that there isn't an active ping call going 81 | // by the time stop returns. 82 | mu.Lock() 83 | defer mu.Unlock() 84 | stopped = true 85 | return pingErr 86 | } 87 | } 88 | 89 | func catchPanics(cb func(context.Context) error) func(context.Context) error { 90 | return func(ctx context.Context) (err error) { 91 | defer func() { 92 | if rec := recover(); rec != nil { 93 | if rerr, ok := rec.(error); ok { 94 | err = rerr 95 | } else { 96 | err = fmt.Errorf("%v", rec) 97 | } 98 | } 99 | }() 100 | return cb(ctx) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /registry.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "sort" 19 | "sync" 20 | ) 21 | 22 | type traceWatcherRef struct { 23 | watcher func(*Trace) 24 | } 25 | 26 | type registryInternal struct { 27 | // sync/atomic things 28 | traceWatcher *traceWatcherRef 29 | 30 | watcherMtx sync.Mutex 31 | watcherCounter int64 32 | traceWatchers map[int64]func(*Trace) 33 | 34 | scopeMtx sync.Mutex 35 | scopes map[string]*Scope 36 | 37 | spanMtx sync.Mutex 38 | spans map[*Span]struct{} 39 | 40 | orphanMtx sync.Mutex 41 | orphans map[*Span]struct{} 42 | } 43 | 44 | // Registry encapsulates all of the top-level state for a monitoring system. 45 | // In general, only the Default registry is ever used. 46 | type Registry struct { 47 | // A *Registry type is actually just a reference handle to *registryInternal. 48 | // There may be multiple different *Registry types pointing at the same 49 | // *registryInternal but with different transformer sets, to make 50 | // WithTransformers work right. 51 | *registryInternal 52 | 53 | transformers []CallbackTransformer 54 | } 55 | 56 | // NewRegistry creates a NewRegistry, though you almost certainly just want 57 | // to use Default. 58 | func NewRegistry() *Registry { 59 | return &Registry{ 60 | registryInternal: ®istryInternal{ 61 | traceWatchers: map[int64]func(*Trace){}, 62 | scopes: map[string]*Scope{}, 63 | spans: map[*Span]struct{}{}, 64 | orphans: map[*Span]struct{}{}}} 65 | } 66 | 67 | // WithTransformers returns a copy of Registry but with the additional 68 | // CallbackTransformers applied to the Stats method. 69 | func (r *Registry) WithTransformers(t ...CallbackTransformer) *Registry { 70 | return &Registry{ 71 | registryInternal: r.registryInternal, 72 | transformers: append(append([]CallbackTransformer(nil), r.transformers...), t...), 73 | } 74 | } 75 | 76 | // Package creates a new monitoring Scope, named after the top level package. 77 | // It's expected that you'll have something like 78 | // 79 | // var mon = monkit.Package() 80 | // 81 | // at the top of each package. 82 | func (r *Registry) Package() *Scope { 83 | return r.ScopeNamed(callerPackage(1)) 84 | } 85 | 86 | // ScopeNamed is like Package, but lets you choose the name. 87 | func (r *Registry) ScopeNamed(name string) *Scope { 88 | r.scopeMtx.Lock() 89 | defer r.scopeMtx.Unlock() 90 | s, exists := r.scopes[name] 91 | if exists { 92 | return s 93 | } 94 | s = newScope(r, name) 95 | r.scopes[name] = s 96 | return s 97 | } 98 | 99 | func (r *Registry) observeTrace(t *Trace) { 100 | watcher := loadTraceWatcherRef(&r.traceWatcher) 101 | if watcher != nil { 102 | watcher.watcher(t) 103 | } 104 | } 105 | 106 | func (r *Registry) updateWatcher() { 107 | cbs := make([]func(*Trace), 0, len(r.traceWatchers)) 108 | for _, cb := range r.traceWatchers { 109 | cbs = append(cbs, cb) 110 | } 111 | switch len(cbs) { 112 | case 0: 113 | storeTraceWatcherRef(&r.traceWatcher, nil) 114 | case 1: 115 | storeTraceWatcherRef(&r.traceWatcher, 116 | &traceWatcherRef{watcher: cbs[0]}) 117 | default: 118 | storeTraceWatcherRef(&r.traceWatcher, 119 | &traceWatcherRef{watcher: func(t *Trace) { 120 | for _, cb := range cbs { 121 | cb(t) 122 | } 123 | }}) 124 | } 125 | } 126 | 127 | // ObserveTraces lets you observe all traces flowing through the system. 128 | // The passed in callback 'cb' will be called for every new trace as soon as 129 | // it starts, until the returned cancel method is called. 130 | // Note: this only applies to all new traces. If you want to find existing 131 | // or running traces, please pull them off of live RootSpans. 132 | func (r *Registry) ObserveTraces(cb func(*Trace)) (cancel func()) { 133 | // even though observeTrace doesn't get a mutex, it's only ever loading 134 | // the traceWatcher pointer, so we can use this mutex here to safely 135 | // coordinate the setting of the traceWatcher pointer. 136 | r.watcherMtx.Lock() 137 | defer r.watcherMtx.Unlock() 138 | 139 | cbId := r.watcherCounter 140 | r.watcherCounter += 1 141 | r.traceWatchers[cbId] = cb 142 | r.updateWatcher() 143 | 144 | return func() { 145 | r.watcherMtx.Lock() 146 | defer r.watcherMtx.Unlock() 147 | delete(r.traceWatchers, cbId) 148 | r.updateWatcher() 149 | } 150 | } 151 | 152 | func (r *Registry) rootSpanStart(s *Span) { 153 | r.spanMtx.Lock() 154 | r.spans[s] = struct{}{} 155 | r.spanMtx.Unlock() 156 | } 157 | 158 | func (r *Registry) rootSpanEnd(s *Span) { 159 | r.spanMtx.Lock() 160 | delete(r.spans, s) 161 | r.spanMtx.Unlock() 162 | } 163 | 164 | func (r *Registry) orphanedSpan(s *Span) { 165 | r.orphanMtx.Lock() 166 | r.orphans[s] = struct{}{} 167 | r.orphanMtx.Unlock() 168 | } 169 | 170 | func (r *Registry) orphanEnd(s *Span) { 171 | r.orphanMtx.Lock() 172 | delete(r.orphans, s) 173 | r.orphanMtx.Unlock() 174 | } 175 | 176 | // RootSpans will call 'cb' on all currently executing Spans with no live or 177 | // reachable parent. See also AllSpans. 178 | func (r *Registry) RootSpans(cb func(s *Span)) { 179 | r.spanMtx.Lock() 180 | spans := make([]*Span, 0, len(r.spans)) 181 | for s := range r.spans { 182 | spans = append(spans, s) 183 | } 184 | r.spanMtx.Unlock() 185 | r.orphanMtx.Lock() 186 | orphans := make([]*Span, 0, len(r.orphans)) 187 | for s := range r.orphans { 188 | orphans = append(orphans, s) 189 | } 190 | r.orphanMtx.Unlock() 191 | spans = append(spans, orphans...) 192 | sort.Sort(spanSorter(spans)) 193 | for _, s := range spans { 194 | cb(s) 195 | } 196 | } 197 | 198 | func walkSpan(s *Span, cb func(s *Span)) { 199 | cb(s) 200 | s.Children(func(s *Span) { 201 | walkSpan(s, cb) 202 | }) 203 | } 204 | 205 | // AllSpans calls 'cb' on all currently known Spans. See also RootSpans. 206 | func (r *Registry) AllSpans(cb func(s *Span)) { 207 | r.RootSpans(func(s *Span) { walkSpan(s, cb) }) 208 | } 209 | 210 | // Scopes calls 'cb' on all currently known Scopes. 211 | func (r *Registry) Scopes(cb func(s *Scope)) { 212 | r.scopeMtx.Lock() 213 | c := make([]*Scope, 0, len(r.scopes)) 214 | for _, s := range r.scopes { 215 | c = append(c, s) 216 | } 217 | r.scopeMtx.Unlock() 218 | sort.Sort(scopeSorter(c)) 219 | for _, s := range c { 220 | cb(s) 221 | } 222 | } 223 | 224 | // Funcs calls 'cb' on all currently known Funcs. 225 | func (r *Registry) Funcs(cb func(f *Func)) { 226 | r.Scopes(func(s *Scope) { s.Funcs(cb) }) 227 | } 228 | 229 | // Stats implements the StatSource interface. 230 | func (r *Registry) Stats(cb func(key SeriesKey, field string, val float64)) { 231 | for _, t := range r.transformers { 232 | cb = t.Transform(cb) 233 | } 234 | r.Scopes(func(s *Scope) { s.Stats(cb) }) 235 | } 236 | 237 | var _ StatSource = (*Registry)(nil) 238 | 239 | // Default is the default Registry 240 | var Default = NewRegistry() 241 | 242 | // ScopeNamed is just a wrapper around Default.ScopeNamed 243 | func ScopeNamed(name string) *Scope { return Default.ScopeNamed(name) } 244 | 245 | // RootSpans is just a wrapper around Default.RootSpans 246 | func RootSpans(cb func(s *Span)) { Default.RootSpans(cb) } 247 | 248 | // Scopes is just a wrapper around Default.Scopes 249 | func Scopes(cb func(s *Scope)) { Default.Scopes(cb) } 250 | 251 | // Funcs is just a wrapper around Default.Funcs 252 | func Funcs(cb func(f *Func)) { Default.Funcs(cb) } 253 | 254 | // Package is just a wrapper around Default.Package 255 | func Package() *Scope { return Default.ScopeNamed(callerPackage(1)) } 256 | 257 | // Stats is just a wrapper around Default.Stats 258 | func Stats(cb func(key SeriesKey, field string, val float64)) { Default.Stats(cb) } 259 | 260 | type spanSorter []*Span 261 | 262 | func (s spanSorter) Len() int { return len(s) } 263 | func (s spanSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 264 | 265 | func (s spanSorter) Less(i, j int) bool { 266 | ispan, jspan := s[i], s[j] 267 | iname, jname := ispan.f.FullName(), jspan.f.FullName() 268 | return (iname < jname) || (iname == jname && ispan.id < jspan.id) 269 | } 270 | 271 | type scopeSorter []*Scope 272 | 273 | func (s scopeSorter) Len() int { return len(s) } 274 | func (s scopeSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 275 | func (s scopeSorter) Less(i, j int) bool { return s[i].name < s[j].name } 276 | -------------------------------------------------------------------------------- /rng.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | 3 | package monkit 4 | 5 | import ( 6 | "math/rand" 7 | ) 8 | 9 | // lcg is a simple linear congruential generator based on Knuths MMIX. 10 | type lcg uint64 11 | 12 | // Make sure lcg is a rand.Source 13 | var _ rand.Source = (*lcg)(nil) 14 | 15 | func newLCG() lcg { return lcg(rand.Int63()) } 16 | 17 | // See Knuth. 18 | const ( 19 | a = 6364136223846793005 20 | c = 1442695040888963407 21 | h = 0xffffffff00000000 22 | ) 23 | 24 | // Uint64 returns a uint64. 25 | func (l *lcg) Uint64() (ret uint64) { 26 | *l = a**l + c 27 | ret |= uint64(*l) >> 32 28 | *l = a**l + c 29 | ret |= uint64(*l) & h 30 | return 31 | } 32 | 33 | // Int63 returns a positive 63 bit integer in an int64 34 | func (l *lcg) Int63() int64 { 35 | return int64(l.Uint64() >> 1) 36 | } 37 | 38 | // Seed sets the state of the lcg. 39 | func (l *lcg) Seed(seed int64) { 40 | *l = lcg(seed) 41 | } 42 | 43 | // 44 | // xorshift family of generators from https://en.wikipedia.org/wiki/Xorshift 45 | // 46 | // xorshift64 is the xorshift64* generator 47 | // xorshift1024 is the xorshift1024* generator 48 | // xorshift128 is the xorshift128+ generator 49 | // 50 | 51 | type xorshift64 uint64 52 | 53 | var _ rand.Source = (*xorshift64)(nil) 54 | 55 | func newXORShift64() xorshift64 { return xorshift64(rand.Int63()) } 56 | 57 | // Uint64 returns a uint64. 58 | func (s *xorshift64) Uint64() (ret uint64) { 59 | x := uint64(*s) 60 | x ^= x >> 12 // a 61 | x ^= x << 25 // b 62 | x ^= x >> 27 // c 63 | x *= 2685821657736338717 64 | *s = xorshift64(x) 65 | return x 66 | } 67 | 68 | // Int63 returns a positive 63 bit integer in an int64 69 | func (s *xorshift64) Int63() int64 { 70 | return int64(s.Uint64() >> 1) 71 | } 72 | 73 | // Seed sets the state of the lcg. 74 | func (s *xorshift64) Seed(seed int64) { 75 | *s = xorshift64(seed) 76 | } 77 | 78 | type xorshift1024 struct { 79 | s [16]uint64 80 | p int 81 | } 82 | 83 | var _ rand.Source = (*xorshift1024)(nil) 84 | 85 | func newXORShift1024() xorshift1024 { 86 | var x xorshift1024 87 | x.Seed(rand.Int63()) 88 | return x 89 | } 90 | 91 | // Seed sets the state of the lcg. 92 | func (s *xorshift1024) Seed(seed int64) { 93 | rng := xorshift64(seed) 94 | *s = xorshift1024{ 95 | s: [16]uint64{ 96 | rng.Uint64(), rng.Uint64(), rng.Uint64(), rng.Uint64(), 97 | rng.Uint64(), rng.Uint64(), rng.Uint64(), rng.Uint64(), 98 | rng.Uint64(), rng.Uint64(), rng.Uint64(), rng.Uint64(), 99 | rng.Uint64(), rng.Uint64(), rng.Uint64(), rng.Uint64(), 100 | }, 101 | p: 0, 102 | } 103 | } 104 | 105 | // Int63 returns a positive 63 bit integer in an int64 106 | func (s *xorshift1024) Int63() int64 { 107 | return int64(s.Uint64() >> 1) 108 | } 109 | 110 | // Uint64 returns a uint64. 111 | func (s *xorshift1024) Uint64() (ret uint64) { 112 | // factoring this out proves to SSA backend that the array checks below 113 | // do not need bounds checks 114 | p := s.p & 15 115 | s0 := s.s[p] 116 | p = (p + 1) & 15 117 | s.p = p 118 | s1 := s.s[p] 119 | s1 ^= s1 << 31 120 | s.s[p] = s1 ^ s0 ^ (s1 >> 1) ^ (s0 >> 30) 121 | return s.s[p] * 1181783497276652981 122 | } 123 | 124 | // Jump is used to advance the state 2^512 iterations. 125 | func (s *xorshift1024) Jump() { 126 | var t [16]uint64 127 | for i := 0; i < 16; i++ { 128 | for b := uint(0); b < 64; b++ { 129 | if (xorshift1024jump[i] & (1 << b)) > 0 { 130 | for j := 0; j < 16; j++ { 131 | t[j] ^= s.s[(j+s.p)&15] 132 | } 133 | } 134 | _ = s.Uint64() 135 | } 136 | } 137 | for j := 0; j < 16; j++ { 138 | s.s[(j+s.p)&15] = t[j] 139 | } 140 | } 141 | 142 | var xorshift1024jump = [16]uint64{ 143 | 0x84242f96eca9c41d, 0xa3c65b8776f96855, 0x5b34a39f070b5837, 144 | 0x4489affce4f31a1e, 0x2ffeeb0a48316f40, 0xdc2d9891fe68c022, 145 | 0x3659132bb12fea70, 0xaac17d8efa43cab8, 0xc4cb815590989b13, 146 | 0x5ee975283d71c93b, 0x691548c86c1bd540, 0x7910c41d10a1e6a5, 147 | 0x0b5fc64563b3e2a8, 0x047f7684e9fc949d, 0xb99181f2d8f685ca, 148 | 0x284600e3f30e38c3, 149 | } 150 | 151 | type xorshift128 [2]uint64 152 | 153 | var _ rand.Source = (*xorshift128)(nil) 154 | 155 | func newXORShift128() xorshift128 { 156 | var s xorshift128 157 | s.Seed(rand.Int63()) 158 | return s 159 | } 160 | 161 | func (s *xorshift128) Seed(seed int64) { 162 | rng := xorshift64(seed) 163 | *s = xorshift128{ 164 | rng.Uint64(), rng.Uint64(), 165 | } 166 | } 167 | 168 | // Int63 returns a positive 63 bit integer in an int64 169 | func (s *xorshift128) Int63() int64 { 170 | return int64(s.Uint64() >> 1) 171 | } 172 | 173 | // Uint64 returns a uint64. 174 | func (s *xorshift128) Uint64() (ret uint64) { 175 | x := s[0] 176 | y := s[1] 177 | s[0] = y 178 | x ^= x << 23 179 | s[1] = x ^ y ^ (x >> 17) ^ (y >> 26) 180 | return s[1] + y 181 | } 182 | -------------------------------------------------------------------------------- /rng_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | 3 | package monkit 4 | 5 | import "testing" 6 | 7 | var sink uint64 8 | 9 | func BenchmarkLCG(b *testing.B) { 10 | l := newLCG() 11 | 12 | b.SetBytes(8) 13 | b.ResetTimer() 14 | 15 | for i := 0; i < b.N; i++ { 16 | sink += l.Uint64() 17 | } 18 | } 19 | 20 | func BenchmarkXORShift64(b *testing.B) { 21 | x := newXORShift64() 22 | 23 | b.SetBytes(8) 24 | b.ResetTimer() 25 | 26 | for i := 0; i < b.N; i++ { 27 | sink += x.Uint64() 28 | } 29 | } 30 | 31 | func BenchmarkXORShift1024(b *testing.B) { 32 | x := newXORShift1024() 33 | 34 | b.SetBytes(8) 35 | b.ResetTimer() 36 | 37 | for i := 0; i < b.N; i++ { 38 | sink += x.Uint64() 39 | } 40 | } 41 | 42 | func BenchmarkXORShift128(b *testing.B) { 43 | x := newXORShift128() 44 | 45 | b.SetBytes(8) 46 | b.ResetTimer() 47 | 48 | for i := 0; i < b.N; i++ { 49 | sink += x.Uint64() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /span.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "encoding/hex" 19 | "fmt" 20 | "sort" 21 | "strconv" 22 | "strings" 23 | "time" 24 | ) 25 | 26 | type ctxKey int 27 | 28 | const ( 29 | spanKey ctxKey = iota 30 | ) 31 | 32 | // Annotation represents an arbitrary name and value string pair 33 | type Annotation struct { 34 | Name string 35 | Value string 36 | } 37 | 38 | func (s *Span) addChild(child *Span) { 39 | s.mtx.Lock() 40 | s.children.Add(child) 41 | done := s.done 42 | s.mtx.Unlock() 43 | if done { 44 | child.orphan() 45 | } 46 | } 47 | 48 | func (s *Span) removeChild(child *Span) { 49 | s.mtx.Lock() 50 | s.children.Remove(child) 51 | s.mtx.Unlock() 52 | } 53 | 54 | func (s *Span) orphan() { 55 | s.mtx.Lock() 56 | if !s.done && !s.orphaned { 57 | s.orphaned = true 58 | s.f.scope.r.orphanedSpan(s) 59 | } 60 | s.mtx.Unlock() 61 | } 62 | 63 | // Duration returns the current amount of time the Span has been running 64 | func (s *Span) Duration() time.Duration { 65 | return time.Since(s.start) 66 | } 67 | 68 | // Start returns the time the Span started. 69 | func (s *Span) Start() time.Time { 70 | return s.start 71 | } 72 | 73 | // Value implements context.Context 74 | func (s *Span) Value(key interface{}) interface{} { 75 | if key == spanKey { 76 | return s 77 | } 78 | return s.Context.Value(key) 79 | } 80 | 81 | // String implements context.Context 82 | func (s *Span) String() string { 83 | // TODO: for working with Contexts 84 | return fmt.Sprintf("%v.WithSpan()", s.Context) 85 | } 86 | 87 | // Children returns all known running child Spans. 88 | func (s *Span) Children(cb func(s *Span)) { 89 | found := map[*Span]bool{} 90 | var sorter []*Span 91 | s.mtx.Lock() 92 | s.children.Iterate(func(s *Span) { 93 | if !found[s] { 94 | found[s] = true 95 | sorter = append(sorter, s) 96 | } 97 | }) 98 | s.mtx.Unlock() 99 | sort.Sort(spanSorter(sorter)) 100 | for _, s := range sorter { 101 | cb(s) 102 | } 103 | } 104 | 105 | // Args returns the list of strings associated with the args given to the 106 | // Task that created this Span. 107 | func (s *Span) Args() (rv []string) { 108 | rv = make([]string, 0, len(s.args)) 109 | for _, arg := range s.args { 110 | switch arg := arg.(type) { 111 | case string: 112 | rv = append(rv, strconv.Quote(arg)) 113 | case []uint8: 114 | rv = append(rv, "[]uint8(0x"+hex.EncodeToString(arg)+")") 115 | case []interface{}: 116 | rv = append(rv, interfacesToString(arg)) 117 | case time.Time: 118 | rv = append(rv, "time.Time("+arg.Format(time.RFC3339Nano)+")") 119 | default: 120 | rv = append(rv, fmt.Sprintf("%#v", arg)) 121 | } 122 | } 123 | return rv 124 | } 125 | 126 | func interfacesToString(args []interface{}) string { 127 | var b strings.Builder 128 | b.WriteString("{") 129 | for i, arg := range args { 130 | if i > 0 { 131 | b.WriteString(", ") 132 | } 133 | switch arg := arg.(type) { 134 | case string: 135 | b.WriteString(strconv.Quote(arg)) 136 | case []uint8: 137 | b.WriteString("[]uint8(0x") 138 | b.WriteString(hex.EncodeToString(arg)) 139 | b.WriteString(")") 140 | case time.Time: 141 | b.WriteString("time.Time(") 142 | b.WriteString(arg.Format(time.RFC3339Nano)) 143 | b.WriteString(")") 144 | default: 145 | _, _ = fmt.Fprintf(&b, "%#v", arg) 146 | } 147 | } 148 | b.WriteString("}") 149 | return b.String() 150 | } 151 | 152 | // Id returns the Span id. 153 | func (s *Span) Id() int64 { return s.id } 154 | 155 | // ParentId returns the id of the parent Span, if it has a parent. 156 | func (s *Span) ParentId() (int64, bool) { 157 | if s.parentId != nil { 158 | return *s.parentId, true 159 | } else if s.parent != nil { 160 | return s.parent.id, true 161 | } 162 | return 0, false 163 | } 164 | 165 | // Func returns the Func that kicked off this Span. 166 | func (s *Span) Func() *Func { return s.f } 167 | 168 | // Trace returns the Trace this Span is associated with. 169 | func (s *Span) Trace() *Trace { return s.trace } 170 | 171 | // Annotations returns any added annotations created through the Span Annotate 172 | // method 173 | func (s *Span) Annotations() []Annotation { 174 | s.mtx.Lock() 175 | annotations := s.annotations // okay cause we only ever append to this slice 176 | s.mtx.Unlock() 177 | return append([]Annotation(nil), annotations...) 178 | } 179 | 180 | // Annotate adds an annotation to the existing Span. 181 | func (s *Span) Annotate(name, val string) { 182 | s.mtx.Lock() 183 | s.annotations = append(s.annotations, Annotation{Name: name, Value: val}) 184 | s.mtx.Unlock() 185 | } 186 | 187 | // Orphaned returns true if the Parent span ended before this Span did. 188 | func (s *Span) Orphaned() (rv bool) { 189 | s.mtx.Lock() 190 | rv = s.orphaned 191 | s.mtx.Unlock() 192 | return rv 193 | } 194 | -------------------------------------------------------------------------------- /spanbag.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | // spanBag is a bag data structure (can add 0 or more references to a span, 18 | // where every add needs to be matched with an equivalent remove). spanBag has 19 | // a fast path for dealing with cases where the bag only has one element (the 20 | // common case). spanBag is not threadsafe 21 | type spanBag struct { 22 | first *Span 23 | rest map[*Span]int32 24 | } 25 | 26 | func (b *spanBag) Add(s *Span) { 27 | if b.first == nil { 28 | b.first = s 29 | return 30 | } 31 | if b.rest == nil { 32 | b.rest = map[*Span]int32{} 33 | } 34 | b.rest[s] += 1 35 | } 36 | 37 | func (b *spanBag) Remove(s *Span) { 38 | if b.first == s { 39 | b.first = nil 40 | return 41 | } 42 | // okay it must be in b.rest 43 | count := b.rest[s] 44 | if count <= 1 { 45 | delete(b.rest, s) 46 | } else { 47 | b.rest[s] = count - 1 48 | } 49 | } 50 | 51 | // Iterate returns all elements 52 | func (b *spanBag) Iterate(cb func(*Span)) { 53 | if b.first != nil { 54 | cb(b.first) 55 | } 56 | for s := range b.rest { 57 | cb(s) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /spinlock.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "runtime" 19 | "sync/atomic" 20 | ) 21 | 22 | type spinLock uint32 23 | 24 | func (s *spinLock) Lock() { 25 | for { 26 | if atomic.CompareAndSwapUint32((*uint32)(s), 0, 1) { 27 | return 28 | } 29 | runtime.Gosched() 30 | } 31 | } 32 | 33 | func (s *spinLock) Unlock() { 34 | atomic.StoreUint32((*uint32)(s), 0) 35 | } 36 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "strings" 19 | ) 20 | 21 | // SeriesKey represents an individual time series for monkit to output. 22 | type SeriesKey struct { 23 | Measurement string 24 | Tags *TagSet 25 | } 26 | 27 | // NewSeriesKey constructs a new series with the minimal fields. 28 | func NewSeriesKey(measurement string) SeriesKey { 29 | return SeriesKey{Measurement: measurement} 30 | } 31 | 32 | // WithTag returns a copy of the SeriesKey with the tag set 33 | func (s SeriesKey) WithTag(key, value string) SeriesKey { 34 | s.Tags = s.Tags.Set(key, value) 35 | return s 36 | } 37 | 38 | // WithTags returns a copy of the SeriesKey with all of the tags set 39 | func (s SeriesKey) WithTags(tags ...SeriesTag) SeriesKey { 40 | s.Tags = s.Tags.SetTags(tags...) 41 | return s 42 | } 43 | 44 | // String returns a string representation of the series. For example, it returns 45 | // something like `measurement,tag0=val0,tag1=val1`. 46 | func (s SeriesKey) String() string { 47 | var builder strings.Builder 48 | writeMeasurement(&builder, s.Measurement) 49 | if s.Tags.Len() > 0 { 50 | builder.WriteByte(',') 51 | builder.WriteString(s.Tags.String()) 52 | } 53 | return builder.String() 54 | } 55 | 56 | func (s SeriesKey) WithField(field string) string { 57 | var builder strings.Builder 58 | builder.WriteString(s.String()) 59 | builder.WriteByte(' ') 60 | writeTag(&builder, field) 61 | return builder.String() 62 | } 63 | 64 | // StatSource represents anything that can return named floating point values. 65 | type StatSource interface { 66 | Stats(cb func(key SeriesKey, field string, val float64)) 67 | } 68 | 69 | type StatSourceFunc func(cb func(key SeriesKey, field string, val float64)) 70 | 71 | func (f StatSourceFunc) Stats(cb func(key SeriesKey, field string, val float64)) { 72 | f(cb) 73 | } 74 | 75 | // Collect takes something that implements the StatSource interface and returns 76 | // a key/value map. 77 | func Collect(mon StatSource) map[string]float64 { 78 | rv := make(map[string]float64) 79 | mon.Stats(func(key SeriesKey, field string, val float64) { 80 | rv[key.WithField(field)] = val 81 | }) 82 | return rv 83 | } 84 | -------------------------------------------------------------------------------- /struct.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "reflect" 19 | "strings" 20 | ) 21 | 22 | var ( 23 | f64Type = reflect.TypeOf(float64(0)) 24 | boolType = reflect.TypeOf(bool(false)) 25 | ) 26 | 27 | type emptyStatSource struct{} 28 | 29 | func (emptyStatSource) Stats(cb func(key SeriesKey, field string, val float64)) {} 30 | 31 | // StatSourceFromStruct uses the reflect package to implement the Stats call 32 | // across all float64-castable and bool-castable fields of the struct. 33 | func StatSourceFromStruct(key SeriesKey, structData interface{}) StatSource { 34 | val := deref(reflect.ValueOf(structData)) 35 | 36 | typ := val.Type() 37 | if typ.Kind() != reflect.Struct { 38 | return emptyStatSource{} 39 | } 40 | 41 | return StatSourceFunc(func(cb func(key SeriesKey, field string, val float64)) { 42 | nextField: 43 | for i := 0; i < typ.NumField(); i++ { 44 | field := deref(val.Field(i)) 45 | field_type := field.Type() 46 | 47 | parts := strings.Split(typ.Field(i).Tag.Get("monkit"), ",") 48 | for _, part := range parts { 49 | if part == "ignore" { 50 | continue nextField 51 | } 52 | } 53 | 54 | if field_type.Kind() == reflect.Struct && field.CanInterface() { 55 | child_source := StatSourceFromStruct(key, field.Interface()) 56 | child_source.Stats(func(key SeriesKey, field string, val float64) { 57 | cb(key, typ.Field(i).Name+"."+field, val) 58 | }) 59 | 60 | } else if field_type.ConvertibleTo(f64Type) { 61 | cb(key, typ.Field(i).Name, field.Convert(f64Type).Float()) 62 | } else if field_type.ConvertibleTo(boolType) { 63 | if field.Convert(boolType).Bool() { 64 | cb(key, typ.Field(i).Name, 1) 65 | } else { 66 | cb(key, typ.Field(i).Name, 0) 67 | } 68 | } 69 | } 70 | }) 71 | } 72 | 73 | // if val is a pointer, deref until it isn't 74 | func deref(val reflect.Value) reflect.Value { 75 | for val.Type().Kind() == reflect.Ptr { 76 | val = val.Elem() 77 | } 78 | return val 79 | } 80 | -------------------------------------------------------------------------------- /struct_test.go: -------------------------------------------------------------------------------- 1 | package monkit 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestStatSourceFromStruct(t *testing.T) { 9 | type SubStruct struct { 10 | SubBool bool 11 | SubFloat float64 12 | SubInt int64 13 | Ignored float64 `monkit:"ignore"` 14 | } 15 | 16 | result := Collect(StatSourceFromStruct(NewSeriesKey("struct"), 17 | struct { 18 | SomeBool bool 19 | SomeFloat float64 20 | SomeInt int64 21 | Ignored float64 `monkit:"ignore"` 22 | Sub SubStruct 23 | Skip struct { 24 | Nope int64 25 | } `monkit:"whatever,ignore"` 26 | }{ 27 | SomeInt: 5, 28 | SomeBool: true, 29 | Sub: SubStruct{ 30 | SubFloat: 3.2, 31 | }, 32 | }, 33 | )) 34 | 35 | if !reflect.DeepEqual(result, map[string]float64{ 36 | "struct SomeBool": 1, 37 | "struct SomeFloat": 0, 38 | "struct SomeInt": 5, 39 | "struct Sub.SubBool": 0, 40 | "struct Sub.SubFloat": 3.2, 41 | "struct Sub.SubInt": 0, 42 | }) { 43 | t.Fatal("unexpected result", result) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "sort" 19 | "strings" 20 | ) 21 | 22 | // SeriesTag is a key/value pair. When used with a measurement name, each set 23 | // of unique key/value pairs represents a new unique series. 24 | type SeriesTag struct { 25 | Key, Val string 26 | } 27 | 28 | // NewTag creates a new tag 29 | func NewSeriesTag(key, val string) SeriesTag { 30 | return SeriesTag{key, val} 31 | } 32 | 33 | // TagSet is an immutible collection of tag, value pairs. 34 | type TagSet struct { 35 | all map[string]string 36 | str string // cached string form 37 | } 38 | 39 | // Get returns the value associated with the key. 40 | func (t *TagSet) Get(key string) string { 41 | if t == nil || t.all == nil { 42 | return "" 43 | } 44 | return t.all[key] 45 | } 46 | 47 | // All returns a map of all the key/value pairs in the tag set. It 48 | // should not be modified. 49 | func (t *TagSet) All() map[string]string { 50 | if t == nil { 51 | return nil 52 | } 53 | return t.all 54 | } 55 | 56 | // Len returns the number of tags in the tag set. 57 | func (t *TagSet) Len() int { 58 | if t == nil { 59 | return 0 60 | } 61 | return len(t.all) 62 | } 63 | 64 | // Set returns a new tag set with the key associated to the value. 65 | func (t *TagSet) Set(key, value string) *TagSet { 66 | return t.SetAll(map[string]string{key: value}) 67 | } 68 | 69 | // SetTags returns a new tag set with the keys and values set by the tags slice. 70 | func (t *TagSet) SetTags(tags ...SeriesTag) *TagSet { 71 | all := make(map[string]string) 72 | if t != nil { 73 | for key, value := range t.all { 74 | all[key] = value 75 | } 76 | } 77 | for _, tag := range tags { 78 | all[tag.Key] = tag.Val 79 | } 80 | return &TagSet{all: all} 81 | } 82 | 83 | // SetAll returns a new tag set with the key value pairs in the map all set. 84 | func (t *TagSet) SetAll(kvs map[string]string) *TagSet { 85 | all := make(map[string]string) 86 | if t != nil { 87 | for key, value := range t.all { 88 | all[key] = value 89 | } 90 | } 91 | for key, value := range kvs { 92 | all[key] = value 93 | } 94 | return &TagSet{all: all} 95 | } 96 | 97 | // String returns a string form of the tag set suitable for sending to influxdb. 98 | func (t *TagSet) String() string { 99 | if t == nil { 100 | return "" 101 | } 102 | if t.str == "" { 103 | var builder strings.Builder 104 | t.writeTags(&builder) 105 | t.str = builder.String() 106 | } 107 | return t.str 108 | } 109 | 110 | // writeTags writes the tags in the tag set to the builder. 111 | func (t *TagSet) writeTags(builder *strings.Builder) { 112 | type kv struct { 113 | key string 114 | value string 115 | } 116 | var kvs []kv 117 | 118 | for key, value := range t.all { 119 | kvs = append(kvs, kv{key, value}) 120 | } 121 | sort.Slice(kvs, func(i, j int) bool { 122 | return kvs[i].key < kvs[j].key 123 | }) 124 | 125 | for i, kv := range kvs { 126 | if i > 0 { 127 | builder.WriteByte(',') 128 | } 129 | writeTag(builder, kv.key) 130 | builder.WriteByte('=') 131 | writeTag(builder, kv.value) 132 | } 133 | } 134 | 135 | // writeMeasurement writes a measurement to the builder. 136 | func writeMeasurement(builder *strings.Builder, measurement string) { 137 | if strings.IndexByte(measurement, ',') == -1 && 138 | strings.IndexByte(measurement, ' ') == -1 { 139 | 140 | builder.WriteString(measurement) 141 | return 142 | } 143 | 144 | for i := 0; i < len(measurement); i++ { 145 | if measurement[i] == ',' || 146 | measurement[i] == ' ' { 147 | builder.WriteByte('\\') 148 | } 149 | builder.WriteByte(measurement[i]) 150 | } 151 | } 152 | 153 | // writeTag writes a tag key, value, or field key to the builder. 154 | func writeTag(builder *strings.Builder, tag string) { 155 | if strings.IndexByte(tag, ',') == -1 && 156 | strings.IndexByte(tag, '=') == -1 && 157 | strings.IndexByte(tag, ' ') == -1 { 158 | 159 | builder.WriteString(tag) 160 | return 161 | } 162 | 163 | for i := 0; i < len(tag); i++ { 164 | if tag[i] == ',' || 165 | tag[i] == '=' || 166 | tag[i] == ' ' { 167 | builder.WriteByte('\\') 168 | } 169 | builder.WriteByte(tag[i]) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tags_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "fmt" 19 | "math/rand" 20 | "reflect" 21 | "testing" 22 | ) 23 | 24 | func TestTagSet(t *testing.T) { 25 | assert := func(ts *TagSet, key string, value string, ok bool) { 26 | t.Helper() 27 | gotValue, gotOk := ts.all[key] 28 | if gotValue != value || gotOk != ok { 29 | t.Fatalf("exp:%q != got:%q || exp:%v != got:%v", value, gotValue, ok, gotOk) 30 | } 31 | } 32 | 33 | ts0 := new(TagSet) 34 | ts0 = ts0.Set("k0", "0") 35 | ts1 := ts0.SetAll(nil) 36 | ts1 = ts1.Set("k0", "1") 37 | ts2 := ts0.SetAll(nil) 38 | ts2 = ts2.Set("k1", "2") 39 | ts0 = ts0.Set("k0", "3") 40 | ts3 := ts0.SetAll(nil) 41 | ts3 = ts3.Set("k0", "4") 42 | ts0 = ts0.Set("k0", "5") 43 | 44 | assert(ts0, "k0", "5", true) 45 | assert(ts0, "k1", "", false) 46 | assert(ts1, "k0", "1", true) 47 | assert(ts1, "k1", "", false) 48 | assert(ts2, "k0", "0", true) 49 | assert(ts2, "k1", "2", true) 50 | assert(ts3, "k0", "4", true) 51 | assert(ts3, "k1", "", false) 52 | 53 | t.Log(ts0) 54 | t.Log(ts1) 55 | t.Log(ts2) 56 | t.Log(ts3) 57 | } 58 | 59 | func TestTagSetFuzz(t *testing.T) { 60 | ts, idx := new(TagSet), 0 61 | tagSets := []*TagSet{ts} 62 | expected := []map[string]string{{}} 63 | 64 | for i := 0; i < 10000; i++ { 65 | switch rand.Intn(10) { 66 | case 0, 1, 2, 3, 4, 5, 6, 7: 67 | key, value := fmt.Sprint(rand.Intn(10)), fmt.Sprint(rand.Intn(10)) 68 | ts = ts.Set(key, value) 69 | tagSets[idx] = ts 70 | expected[idx][key] = value 71 | 72 | case 8: 73 | idx = rand.Intn(len(expected)) 74 | ts = tagSets[idx] 75 | 76 | case 9: 77 | cloned := make(map[string]string, len(expected[idx])) 78 | for key, value := range expected[idx] { 79 | cloned[key] = value 80 | } 81 | 82 | ts = ts.SetAll(nil) 83 | tagSets = append(tagSets, ts) 84 | expected = append(expected, cloned) 85 | idx = len(tagSets) - 1 86 | } 87 | } 88 | 89 | for i := range tagSets { 90 | if got := tagSets[i].all; !reflect.DeepEqual(expected[i], got) { 91 | t.Fatal("mismatch: exp:", expected[i], "got:", got) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /task.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | type taskKey int 22 | 23 | const taskGetFunc taskKey = 0 24 | 25 | type taskSecretT struct{} 26 | 27 | func (*taskSecretT) Value(key interface{}) interface{} { return nil } 28 | func (*taskSecretT) Done() <-chan struct{} { return nil } 29 | func (*taskSecretT) Err() error { return nil } 30 | func (*taskSecretT) Deadline() (time.Time, bool) { 31 | return time.Time{}, false 32 | } 33 | 34 | // Func returns the Func associated with the Task 35 | func (f Task) Func() (out *Func) { 36 | // we're doing crazy things to make a function have methods that do other 37 | // things with internal state. basically, we have a secret argument we can 38 | // pass to the function that is only checked if ctx is taskSecret ( 39 | // which it should never be) that controls what other behavior we want. 40 | // in this case, if arg[0] is taskGetFunc, then f will place the func in the 41 | // out location. 42 | // since someone can cast any function of this signature to a lazy task, 43 | // let's make sure we got roughly expected behavior and panic otherwise 44 | if f(&taskSecret, taskGetFunc, &out) != nil || out == nil { 45 | panic("Func() called on a non-Task function") 46 | } 47 | return out 48 | } 49 | 50 | func taskArgs(f *Func, args []interface{}) bool { 51 | // this function essentially does method dispatch for Tasks. returns true 52 | // if a method got dispatched and normal behavior should be aborted 53 | if len(args) != 2 { 54 | return false 55 | } 56 | val, ok := args[0].(taskKey) 57 | if !ok { 58 | return false 59 | } 60 | switch val { 61 | case taskGetFunc: 62 | *(args[1].(**Func)) = f 63 | return true 64 | } 65 | return false 66 | } 67 | 68 | // TaskNamed is like Task except you can choose the name of the associated 69 | // Func. 70 | // 71 | // You may also include any SeriesTags which should be included with the Task. 72 | func (s *Scope) TaskNamed(name string, tags ...SeriesTag) Task { 73 | return s.FuncNamed(name, tags...).Task 74 | } 75 | -------------------------------------------------------------------------------- /timer.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "sync" 19 | "time" 20 | 21 | "github.com/spacemonkeygo/monkit/v3/monotime" 22 | ) 23 | 24 | // Timer is a threadsafe convenience wrapper around a DurationDist. You should 25 | // construct with NewTimer(), though the expected usage is from a Scope like 26 | // so: 27 | // 28 | // var mon = monkit.Package() 29 | // 30 | // func MyFunc() { 31 | // ... 32 | // timer := mon.Timer("event") 33 | // // perform event 34 | // timer.Stop() 35 | // ... 36 | // } 37 | // 38 | // Timers implement StatSource. 39 | type Timer struct { 40 | mtx sync.Mutex 41 | times *DurationDist 42 | } 43 | 44 | // NewTimer constructs a new Timer. 45 | func NewTimer(key SeriesKey) *Timer { 46 | return &Timer{times: NewDurationDist(key)} 47 | } 48 | 49 | // Start constructs a RunningTimer 50 | func (t *Timer) Start() *RunningTimer { 51 | return &RunningTimer{ 52 | start: monotime.Now(), 53 | t: t} 54 | } 55 | 56 | // RunningTimer should be constructed from a Timer. 57 | type RunningTimer struct { 58 | start time.Time 59 | t *Timer 60 | stopped bool 61 | } 62 | 63 | // Elapsed just returns the amount of time since the timer started 64 | func (r *RunningTimer) Elapsed() time.Duration { 65 | return time.Since(r.start) 66 | } 67 | 68 | // Stop stops the timer, adds the duration to the statistics information, and 69 | // returns the elapsed time. 70 | func (r *RunningTimer) Stop() time.Duration { 71 | elapsed := r.Elapsed() 72 | r.t.mtx.Lock() 73 | if !r.stopped { 74 | r.t.times.Insert(elapsed) 75 | r.stopped = true 76 | } 77 | r.t.mtx.Unlock() 78 | return elapsed 79 | } 80 | 81 | // Values returns the main timer values 82 | func (t *Timer) Values() *DurationDist { 83 | t.mtx.Lock() 84 | rv := t.times.Copy() 85 | t.mtx.Unlock() 86 | return rv 87 | } 88 | 89 | // Stats implements the StatSource interface 90 | func (t *Timer) Stats(cb func(key SeriesKey, field string, val float64)) { 91 | t.mtx.Lock() 92 | times := t.times.Copy() 93 | t.mtx.Unlock() 94 | 95 | times.Stats(cb) 96 | } 97 | -------------------------------------------------------------------------------- /trace.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "sync" 19 | "sync/atomic" 20 | "time" 21 | ) 22 | 23 | // SpanObserver is the interface plugins must implement if they want to observe 24 | // all spans on a given trace as they happen. 25 | type SpanObserver interface { 26 | // Start is called when a Span starts 27 | Start(s *Span) 28 | 29 | // Finish is called when a Span finishes, along with an error if any, whether 30 | // or not it panicked, and what time it finished. 31 | Finish(s *Span, err error, panicked bool, finish time.Time) 32 | } 33 | 34 | // Trace represents a 'trace' of execution. A 'trace' is the collection of all 35 | // of the 'spans' kicked off from the same root execution context. A trace is 36 | // a concurrency-supporting analog of a stack trace, where a span is somewhat 37 | // like a stack frame. 38 | type Trace struct { 39 | // sync/atomic things 40 | spanCount int64 41 | spanObservers *spanObserverTuple 42 | 43 | // immutable things from construction 44 | id int64 45 | 46 | // protected by mtx 47 | mtx sync.Mutex 48 | vals map[interface{}]interface{} 49 | } 50 | 51 | // NewTrace creates a new Trace. 52 | func NewTrace(id int64) *Trace { 53 | return &Trace{id: id} 54 | } 55 | 56 | func (t *Trace) getObserver() SpanCtxObserver { 57 | observers := loadSpanObserverTuple(&t.spanObservers) 58 | if observers == nil { 59 | return nil 60 | } 61 | if loadSpanObserverTuple(&observers.cdr) == nil { 62 | return observers.car 63 | } 64 | return observers 65 | } 66 | 67 | // ObserveSpans lets you register a SpanObserver for all future Spans on the 68 | // Trace. The returned cancel method will remove your observer from the trace. 69 | func (t *Trace) ObserveSpans(observer SpanObserver) (cancel func()) { 70 | return t.ObserveSpansCtx(spanObserverToSpanCtxObserver{observer: observer}) 71 | } 72 | 73 | // ObserveSpansCtx lets you register a SpanCtxObserver for all future Spans on the 74 | // Trace. The returned cancel method will remove your observer from the trace. 75 | func (t *Trace) ObserveSpansCtx(observer SpanCtxObserver) (cancel func()) { 76 | for { 77 | existing := loadSpanObserverTuple(&t.spanObservers) 78 | ref := &spanObserverTuple{car: observer, cdr: existing} 79 | if compareAndSwapSpanObserverTuple(&t.spanObservers, existing, ref) { 80 | return func() { t.removeObserver(ref) } 81 | } 82 | } 83 | } 84 | 85 | func (t *Trace) removeObserver(ref *spanObserverTuple) { 86 | t.mtx.Lock() 87 | defer t.mtx.Unlock() 88 | for { 89 | if removeObserverFrom(&t.spanObservers, ref) { 90 | return 91 | } 92 | } 93 | } 94 | 95 | func removeObserverFrom(parent **spanObserverTuple, ref *spanObserverTuple) ( 96 | success bool) { 97 | existing := loadSpanObserverTuple(parent) 98 | if existing == nil { 99 | return true 100 | } 101 | if existing != ref { 102 | return removeObserverFrom(&existing.cdr, ref) 103 | } 104 | return compareAndSwapSpanObserverTuple(parent, existing, 105 | loadSpanObserverTuple(&existing.cdr)) 106 | } 107 | 108 | // Id returns the id of the Trace 109 | func (t *Trace) Id() int64 { return t.id } 110 | 111 | // GetAll returns values associated with a trace. See SetAll. 112 | func (t *Trace) GetAll() (val map[interface{}]interface{}) { 113 | t.mtx.Lock() 114 | defer t.mtx.Unlock() 115 | new := make(map[interface{}]interface{}, len(t.vals)) 116 | for k, v := range t.vals { 117 | new[k] = v 118 | } 119 | return new 120 | } 121 | 122 | // Get returns a value associated with a key on a trace. See Set. 123 | func (t *Trace) Get(key interface{}) (val interface{}) { 124 | t.mtx.Lock() 125 | if t.vals != nil { 126 | val = t.vals[key] 127 | } 128 | t.mtx.Unlock() 129 | return val 130 | } 131 | 132 | // Set sets a value associated with a key on a trace. See Get. 133 | func (t *Trace) Set(key, val interface{}) { 134 | t.mtx.Lock() 135 | if t.vals == nil { 136 | t.vals = map[interface{}]interface{}{key: val} 137 | } else { 138 | t.vals[key] = val 139 | } 140 | t.mtx.Unlock() 141 | } 142 | 143 | func (t *Trace) incrementSpans() { atomic.AddInt64(&t.spanCount, 1) } 144 | func (t *Trace) decrementSpans() { atomic.AddInt64(&t.spanCount, -1) } 145 | 146 | // Spans returns the number of spans currently associated with the Trace. 147 | func (t *Trace) Spans() int64 { return atomic.LoadInt64(&t.spanCount) } 148 | -------------------------------------------------------------------------------- /transform.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 Storj Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "sync" 19 | ) 20 | 21 | // CallbackTransformer will take a provided callback and return a transformed one. 22 | type CallbackTransformer interface { 23 | Transform(func(SeriesKey, string, float64)) func(SeriesKey, string, float64) 24 | } 25 | 26 | // CallbackTransformerFunc is a single function that implements 27 | // CallbackTransformer's Transform. 28 | type CallbackTransformerFunc func(func(SeriesKey, string, float64)) func(SeriesKey, string, float64) 29 | 30 | // Transform implements CallbackTransformer. 31 | func (f CallbackTransformerFunc) Transform(cb func(SeriesKey, string, float64)) func(SeriesKey, string, float64) { 32 | return f(cb) 33 | } 34 | 35 | // TransformStatSource will make sure that a StatSource has the provided 36 | // CallbackTransformers applied to callbacks given to the StatSource. 37 | func TransformStatSource(s StatSource, transformers ...CallbackTransformer) StatSource { 38 | return StatSourceFunc(func(cb func(key SeriesKey, field string, val float64)) { 39 | for _, t := range transformers { 40 | cb = t.Transform(cb) 41 | } 42 | s.Stats(cb) 43 | }) 44 | } 45 | 46 | // DeltaTransformer calculates deltas from any total fields. It keeps internal 47 | // state to keep track of the previous totals, so care should be taken to use 48 | // a different DeltaTransformer per output. 49 | type DeltaTransformer struct { 50 | mtx sync.Mutex 51 | lastTotals map[string]float64 52 | } 53 | 54 | // NewDeltaTransformer creates a new DeltaTransformer with its own idea of the 55 | // last totals seen. 56 | func NewDeltaTransformer() *DeltaTransformer { 57 | return &DeltaTransformer{lastTotals: map[string]float64{}} 58 | } 59 | 60 | // Transform implements CallbackTransformer. 61 | func (dt *DeltaTransformer) Transform(cb func(SeriesKey, string, float64)) func(SeriesKey, string, float64) { 62 | return func(key SeriesKey, field string, val float64) { 63 | if field != "total" { 64 | cb(key, field, val) 65 | return 66 | } 67 | 68 | mapIndex := key.WithField(field) 69 | 70 | dt.mtx.Lock() 71 | lastTotal, found := dt.lastTotals[mapIndex] 72 | dt.lastTotals[mapIndex] = val 73 | dt.mtx.Unlock() 74 | 75 | cb(key, field, val) 76 | if found { 77 | cb(key, "delta", val-lastTotal) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /val.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015 Space Monkey, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package monkit 16 | 17 | import ( 18 | "sync" 19 | "sync/atomic" 20 | "time" 21 | ) 22 | 23 | // IntVal is a convenience wrapper around an IntDist. Constructed using 24 | // NewIntVal, though its expected usage is like: 25 | // 26 | // var mon = monkit.Package() 27 | // 28 | // func MyFunc() { 29 | // ... 30 | // mon.IntVal("size").Observe(val) 31 | // ... 32 | // } 33 | type IntVal struct { 34 | mtx sync.Mutex 35 | dist IntDist 36 | } 37 | 38 | // NewIntVal creates an IntVal 39 | func NewIntVal(key SeriesKey) (v *IntVal) { 40 | v = &IntVal{} 41 | initIntDist(&v.dist, key) 42 | return v 43 | } 44 | 45 | // Observe observes an integer value 46 | func (v *IntVal) Observe(val int64) { 47 | v.mtx.Lock() 48 | v.dist.Insert(val) 49 | v.mtx.Unlock() 50 | } 51 | 52 | // Stats implements the StatSource interface. 53 | func (v *IntVal) Stats(cb func(key SeriesKey, field string, val float64)) { 54 | v.mtx.Lock() 55 | vd := v.dist.Copy() 56 | v.mtx.Unlock() 57 | 58 | vd.Stats(cb) 59 | } 60 | 61 | // Quantile returns an estimate of the requested quantile of observed values. 62 | // 0 <= quantile <= 1 63 | func (v *IntVal) Quantile(quantile float64) (rv int64) { 64 | v.mtx.Lock() 65 | rv = v.dist.Query(quantile) 66 | v.mtx.Unlock() 67 | return rv 68 | } 69 | 70 | // FloatVal is a convenience wrapper around an FloatDist. Constructed using 71 | // NewFloatVal, though its expected usage is like: 72 | // 73 | // var mon = monkit.Package() 74 | // 75 | // func MyFunc() { 76 | // ... 77 | // mon.FloatVal("size").Observe(val) 78 | // ... 79 | // } 80 | type FloatVal struct { 81 | mtx sync.Mutex 82 | dist FloatDist 83 | } 84 | 85 | // NewFloatVal creates a FloatVal 86 | func NewFloatVal(key SeriesKey) (v *FloatVal) { 87 | v = &FloatVal{} 88 | initFloatDist(&v.dist, key) 89 | return v 90 | } 91 | 92 | // Observe observes an floating point value 93 | func (v *FloatVal) Observe(val float64) { 94 | v.mtx.Lock() 95 | v.dist.Insert(val) 96 | v.mtx.Unlock() 97 | } 98 | 99 | // Stats implements the StatSource interface. 100 | func (v *FloatVal) Stats(cb func(key SeriesKey, field string, val float64)) { 101 | v.mtx.Lock() 102 | vd := v.dist.Copy() 103 | v.mtx.Unlock() 104 | 105 | vd.Stats(cb) 106 | } 107 | 108 | // Quantile returns an estimate of the requested quantile of observed values. 109 | // 0 <= quantile <= 1 110 | func (v *FloatVal) Quantile(quantile float64) (rv float64) { 111 | v.mtx.Lock() 112 | rv = v.dist.Query(quantile) 113 | v.mtx.Unlock() 114 | return rv 115 | } 116 | 117 | // BoolVal keeps statistics about boolean values. It keeps the number of trues, 118 | // number of falses, and the disposition (number of trues minus number of 119 | // falses). Constructed using NewBoolVal, though its expected usage is like: 120 | // 121 | // var mon = monkit.Package() 122 | // 123 | // func MyFunc() { 124 | // ... 125 | // mon.BoolVal("flipped").Observe(bool) 126 | // ... 127 | // } 128 | type BoolVal struct { 129 | trues int64 130 | falses int64 131 | recent int32 132 | key SeriesKey 133 | } 134 | 135 | // NewBoolVal creates a BoolVal 136 | func NewBoolVal(key SeriesKey) *BoolVal { 137 | return &BoolVal{key: key} 138 | } 139 | 140 | // Observe observes a boolean value 141 | func (v *BoolVal) Observe(val bool) { 142 | if val { 143 | atomic.AddInt64(&v.trues, 1) 144 | atomic.StoreInt32(&v.recent, 1) 145 | } else { 146 | atomic.AddInt64(&v.falses, 1) 147 | atomic.StoreInt32(&v.recent, 0) 148 | } 149 | } 150 | 151 | // Stats implements the StatSource interface. 152 | func (v *BoolVal) Stats(cb func(key SeriesKey, field string, val float64)) { 153 | trues := atomic.LoadInt64(&v.trues) 154 | falses := atomic.LoadInt64(&v.falses) 155 | recent := atomic.LoadInt32(&v.recent) 156 | cb(v.key, "disposition", float64(trues-falses)) 157 | cb(v.key, "false", float64(falses)) 158 | cb(v.key, "recent", float64(recent)) 159 | cb(v.key, "true", float64(trues)) 160 | } 161 | 162 | // StructVal keeps track of a structure of data. Constructed using 163 | // NewStructVal, though its expected usage is like: 164 | // 165 | // var mon = monkit.Package() 166 | // 167 | // func MyFunc() { 168 | // ... 169 | // mon.StructVal("stats").Observe(stats) 170 | // ... 171 | // } 172 | type StructVal struct { 173 | mtx sync.Mutex 174 | recent interface{} 175 | key SeriesKey 176 | } 177 | 178 | // NewStructVal creates a StructVal 179 | func NewStructVal(key SeriesKey) *StructVal { 180 | return &StructVal{key: key} 181 | } 182 | 183 | // Observe observes a struct value. Only the fields convertable to float64 will 184 | // be monitored. A reference to the most recently called Observe value is kept 185 | // for reading when Stats is called. 186 | func (v *StructVal) Observe(val interface{}) { 187 | v.mtx.Lock() 188 | v.recent = val 189 | v.mtx.Unlock() 190 | } 191 | 192 | // Stats implements the StatSource interface. 193 | func (v *StructVal) Stats(cb func(key SeriesKey, field string, val float64)) { 194 | v.mtx.Lock() 195 | recent := v.recent 196 | v.mtx.Unlock() 197 | 198 | if recent != nil { 199 | StatSourceFromStruct(v.key, recent).Stats(cb) 200 | } 201 | } 202 | 203 | // DurationVal is a convenience wrapper around an DurationVal. Constructed using 204 | // NewDurationVal, though its expected usage is like: 205 | // 206 | // var mon = monkit.Package() 207 | // 208 | // func MyFunc() { 209 | // ... 210 | // mon.DurationVal("time").Observe(val) 211 | // ... 212 | // } 213 | type DurationVal struct { 214 | mtx sync.Mutex 215 | dist DurationDist 216 | } 217 | 218 | // NewDurationVal creates an DurationVal 219 | func NewDurationVal(key SeriesKey) (v *DurationVal) { 220 | v = &DurationVal{} 221 | initDurationDist(&v.dist, key) 222 | return v 223 | } 224 | 225 | // Observe observes an integer value 226 | func (v *DurationVal) Observe(val time.Duration) { 227 | v.mtx.Lock() 228 | v.dist.Insert(val) 229 | v.mtx.Unlock() 230 | } 231 | 232 | // Stats implements the StatSource interface. 233 | func (v *DurationVal) Stats(cb func(key SeriesKey, field string, val float64)) { 234 | v.mtx.Lock() 235 | vd := v.dist.Copy() 236 | v.mtx.Unlock() 237 | 238 | vd.Stats(cb) 239 | } 240 | 241 | // Quantile returns an estimate of the requested quantile of observed values. 242 | // 0 <= quantile <= 1 243 | func (v *DurationVal) Quantile(quantile float64) (rv time.Duration) { 244 | v.mtx.Lock() 245 | rv = v.dist.Query(quantile) 246 | v.mtx.Unlock() 247 | return rv 248 | } 249 | 250 | // Aggregate can implement additional aggregation for collected values. 251 | type Aggregate func() (observe func(val float64), stat func() (field string, val float64)) 252 | 253 | // RawVal is a simple wrapper around a float64 value without any aggregation 254 | // (histogram, sum, etc). Constructed using NewRawVal, though its expected usage is like: 255 | // 256 | // var mon = monkit.Package() 257 | // 258 | // func MyFunc() { 259 | // ... 260 | // mon.RawVal("value").Observe(val) 261 | // ... 262 | // } 263 | type RawVal struct { 264 | mtx sync.Mutex 265 | value float64 266 | key SeriesKey 267 | stats []func() (field string, val float64) 268 | observers []func(val float64) 269 | } 270 | 271 | // NewRawVal creates a RawVal 272 | func NewRawVal(key SeriesKey, aggregations ...Aggregate) *RawVal { 273 | val := &RawVal{key: key} 274 | for _, agg := range aggregations { 275 | observe, stat := agg() 276 | val.stats = append(val.stats, stat) 277 | val.observers = append(val.observers, observe) 278 | } 279 | return val 280 | } 281 | 282 | // Observe sets the current value 283 | func (v *RawVal) Observe(val float64) { 284 | v.mtx.Lock() 285 | v.value = val 286 | for _, o := range v.observers { 287 | o(val) 288 | } 289 | v.mtx.Unlock() 290 | } 291 | 292 | // Stats implements the StatSource interface. 293 | func (v *RawVal) Stats(cb func(key SeriesKey, field string, val float64)) { 294 | v.mtx.Lock() 295 | cb(v.key, "recent", v.value) 296 | v.mtx.Unlock() 297 | for _, s := range v.stats { 298 | field, value := s() 299 | cb(v.key, field, value) 300 | } 301 | } 302 | 303 | // Get returns the current value 304 | func (v *RawVal) Get() float64 { 305 | v.mtx.Lock() 306 | value := v.value 307 | v.mtx.Unlock() 308 | return value 309 | } 310 | 311 | // Count is a value aggregator that counts the number of times the value is measured. 312 | func Count() (observe func(val float64), stat func() (field string, val float64)) { 313 | var counter int 314 | return func(val float64) { 315 | counter++ 316 | }, func() (field string, val float64) { 317 | return "count", float64(counter) 318 | } 319 | } 320 | 321 | // Sum is a value aggregator that summarizes the values measured. 322 | func Sum() (observe func(val float64), stat func() (field string, val float64)) { 323 | var sum int 324 | return func(val float64) { 325 | sum += int(val) 326 | }, func() (field string, val float64) { 327 | return "sum", float64(sum) 328 | } 329 | } 330 | --------------------------------------------------------------------------------