├── .dockerignore ├── .github └── workflows │ ├── go.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.json ├── .mailmap ├── .otel └── config.yaml ├── AUTHORS.txt ├── Dockerfile ├── HISTORY.md ├── LICENSE ├── Makefile ├── README.md ├── Vagrantfile ├── buckets.go ├── buffer.go ├── clock.go ├── cmd └── dogstatsd │ └── main.go ├── context.go ├── context_test.go ├── datadog ├── append.go ├── append_test.go ├── client.go ├── client_test.go ├── event.go ├── event_test.go ├── metric.go ├── metric_test.go ├── parse.go ├── parse_test.go ├── serializer.go ├── serializer_test.go ├── server.go ├── server_test.go ├── udp.go ├── uds.go └── uds_test.go ├── debugstats ├── debugstats.go └── debugstats_test.go ├── doc.go ├── docker-compose.yml ├── engine.go ├── engine_test.go ├── example_test.go ├── field.go ├── field_test.go ├── go.mod ├── go.sum ├── grafana ├── annotations.go ├── annotations_test.go ├── grafanatest │ ├── annotations.go │ ├── annotations_test.go │ ├── query.go │ ├── query_test.go │ ├── search.go │ └── search_test.go ├── handler.go ├── query.go ├── query_test.go ├── search.go └── search_test.go ├── handler.go ├── handler_test.go ├── httpstats ├── context.go ├── context_test.go ├── handler.go ├── handler_test.go ├── metrics.go ├── metrics_test.go ├── transport.go └── transport_test.go ├── influxdb ├── client.go ├── client_test.go ├── measure.go └── measure_test.go ├── iostats ├── io.go └── io_test.go ├── measure.go ├── measure_test.go ├── netstats ├── conn.go ├── conn_test.go ├── handler.go ├── handler_test.go ├── listener.go └── listener_test.go ├── otlp ├── client.go ├── go.mod ├── go.sum ├── handler.go ├── measure.go ├── metric.go └── otlp_test.go ├── procstats ├── collector.go ├── collector_test.go ├── delaystats.go ├── delaystats_darwin.go ├── delaystats_linux.go ├── delaystats_test.go ├── delaystats_windows.go ├── error.go ├── error_test.go ├── go.go ├── go_test.go ├── linux │ ├── cgroup.go │ ├── cgroup_darwin_test.go │ ├── cgroup_linux_test.go │ ├── error.go │ ├── error_test.go │ ├── files.go │ ├── files_darwin_test.go │ ├── files_linux_test.go │ ├── io.go │ ├── io_test.go │ ├── limits.go │ ├── limits_darwin_test.go │ ├── limits_linux_test.go │ ├── limits_test.go │ ├── memory.go │ ├── memory_darwin.go │ ├── memory_darwin_test.go │ ├── memory_linux.go │ ├── memory_linux_test.go │ ├── parse.go │ ├── parse_test.go │ ├── sched.go │ ├── sched_darwin_test.go │ ├── sched_linux_test.go │ ├── sched_test.go │ ├── stat.go │ ├── stat_darwin_test.go │ ├── stat_linux_test.go │ ├── stat_test.go │ ├── statm.go │ ├── statm_darwin_test.go │ ├── statm_linux_test.go │ └── statm_test.go ├── proc.go ├── proc_darwin.go ├── proc_linux.go ├── proc_test.go └── proc_windows.go ├── prometheus ├── append.go ├── append_test.go ├── handler.go ├── handler_test.go ├── label.go ├── label_test.go ├── metric.go └── metric_test.go ├── reflect.go ├── statstest └── handler.go ├── tag.go ├── tag_test.go ├── util └── objconv │ ├── LICENSE │ ├── README.md │ ├── adapter.go │ ├── codec.go │ ├── decode.go │ ├── decode_test.go │ ├── emit.go │ ├── encode.go │ ├── encode_test.go │ ├── error.go │ ├── json │ ├── bench_test.go │ ├── decode.go │ ├── emit.go │ ├── encode.go │ ├── init.go │ ├── json_test.go │ └── parse.go │ ├── objtests │ └── test_codec.go │ ├── objutil │ ├── duration.go │ ├── duration_test.go │ ├── empty.go │ ├── empty_test.go │ ├── int.go │ ├── int_test.go │ ├── limits.go │ ├── limits_test.go │ ├── tag.go │ ├── tag_test.go │ ├── zero.go │ └── zero_test.go │ ├── parse.go │ ├── sort.go │ ├── struct.go │ ├── struct_test.go │ └── value.go ├── value.go ├── value_test.go ├── veneur ├── client.go └── client_test.go └── version ├── version.go └── version_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .circleci 3 | .buildkite 4 | .github 5 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | "on": 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | go: 12 | - 'oldstable' 13 | - 'stable' 14 | label: 15 | - ubuntu-latest 16 | 17 | runs-on: ${{ matrix.label }} 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Setup Go (${{ matrix.go }}) 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: ${{ matrix.go }} 26 | 27 | - name: Identify OS 28 | run: uname -a 29 | 30 | - name: Identify Go Version 31 | run: go version 32 | 33 | - name: Download Dependencies 34 | run: go mod download 35 | 36 | - name: Run Tests 37 | run: go test -trimpath -race ./... 38 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: golangci-lint 3 | "on": 4 | push: 5 | tags: 6 | - v* 7 | paths: 8 | - '**.go' 9 | - .golangci.yml 10 | - .github/workflows/golangci-lint.yml 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | 16 | jobs: 17 | lint: 18 | name: lint 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version: '>=1.20' 24 | 25 | - uses: actions/checkout@v4 26 | 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v8 29 | with: 30 | version: v2.1.6 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | vendor/*/ 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | # Emacs 28 | *~ 29 | 30 | # Commands 31 | /dogstatsd -------------------------------------------------------------------------------- /.golangci.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatters": { 3 | "enable": [ 4 | "gofumpt", 5 | "goimports" 6 | ], 7 | "exclusions": { 8 | "generated": "lax", 9 | "paths": [ 10 | "^.*\\.(pb|y)\\.go$", 11 | "third_party$", 12 | "builtin$", 13 | "examples$" 14 | ] 15 | }, 16 | "settings": { 17 | "gofumpt": { 18 | "extra-rules": true 19 | }, 20 | "goimports": { 21 | "local-prefixes": [ 22 | "github.com/segmentio/stats" 23 | ] 24 | } 25 | } 26 | }, 27 | "linters": { 28 | "disable": [ 29 | "depguard" 30 | ], 31 | "enable": [ 32 | "godot", 33 | "misspell", 34 | "revive", 35 | "whitespace" 36 | ], 37 | "exclusions": { 38 | "generated": "lax", 39 | "paths": [ 40 | "^.*\\.(pb|y)\\.go$", 41 | "third_party$", 42 | "builtin$", 43 | "examples$" 44 | ], 45 | "presets": [ 46 | "comments", 47 | "common-false-positives", 48 | "legacy", 49 | "std-error-handling" 50 | ], 51 | "rules": [ 52 | { 53 | "linters": [ 54 | "errcheck" 55 | ], 56 | "path": "_test.go" 57 | } 58 | ] 59 | }, 60 | "settings": { 61 | "misspell": { 62 | "locale": "US" 63 | } 64 | } 65 | }, 66 | "version": "2" 67 | } 68 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Daniel Fuentes 2 | Amir Abu Shareb Amir Abushareb 3 | Achille Roussel Achille 4 | Achille Roussel Achille Roussel 5 | Achille Roussel Achille 6 | John Boggs 7 | Michael S. Fischer 8 | Ray Jenkins 9 | Alan Braithwaite 10 | Hyonjee Joo <5000208+hjoo@users.noreply.github.com> 11 | Julien Fabre 12 | -------------------------------------------------------------------------------- /.otel/config.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | http: 6 | endpoint: 0.0.0.0:4318 7 | 8 | processors: 9 | batch: 10 | 11 | exporters: 12 | logging: 13 | logLevel: debug 14 | 15 | prometheus: 16 | endpoint: "0.0.0.0:4319" 17 | 18 | service: 19 | telemetry: 20 | logs: 21 | level: "debug" 22 | 23 | pipelines: 24 | metrics: 25 | receivers: [otlp] 26 | processors: [] 27 | exporters: [logging, prometheus] 28 | 29 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Abhinav Sureka <7731608+ABHINAV-SUREKA@users.noreply.github.com> 2 | Achille Roussel 3 | Alan Braithwaite 4 | Amir Abu Shareb 5 | Bill Havanki 6 | Clement Rey 7 | Colin 8 | Collin Van Dyck 9 | Daniel Fuentes 10 | David Betts 11 | David Scrobonia 12 | Dean Karn 13 | Dominic Barnes 14 | Erik Weathers 15 | Hyonjee Joo <5000208+hjoo@users.noreply.github.com> 16 | Jeremy Jackins 17 | John Boggs 18 | Joseph Herlant 19 | Julien Fabre 20 | Kevin Burke 21 | Kevin Gillette 22 | Matt Layher 23 | Matthew Hooker 24 | Michael S. Fischer 25 | Mike Nichols 26 | Prateek Srivastava 27 | Ray Jenkins 28 | Rick Branson 29 | Rob McQueen 30 | Rohit Garg 31 | Thomas Meson 32 | Tyson Mote 33 | Ulysse Carion 34 | Vic Vijayakumar 35 | Yerden Zhumabekov 36 | dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 37 | noctarius aka Christoph Engelbert 38 | sungjujin <87332309+sungjujin@users.noreply.github.com> 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | 3 | COPY . /go/src/github.com/segmentio/stats 4 | 5 | ENV CGO_ENABLED=0 6 | RUN apk add --no-cache git && \ 7 | cd /go/src/github.com/segmentio/stats && \ 8 | go build -v -o /dogstatsd ./cmd/dogstatsd && \ 9 | apk del git && \ 10 | rm -rf /go/* 11 | 12 | ENTRYPOINT ["/dogstatsd"] 13 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ### v5.6.1 (May 29, 2025) 4 | 5 | Fix an error in the v5.6.0 release related to metric names with a longer buffer 6 | size. 7 | 8 | ### v5.6.0 (May 27, 2025) 9 | 10 | - In the `datadog` library: invalid characters in metric names, field 11 | names, or tag keys/values will be replaced with underscores. Accents and 12 | other diacritics will be removed (e.g. é will be replaced with 'e'). 13 | This change also improves performance of HandleMeasures by about 15-20%. 14 | [#192](https://github.com/segmentio/stats/pull/192) 15 | 16 | - External calls to github.com/segmentio/objconv were replaced by imports of 17 | github.com/segmentio/stats/v5/util/objconv, which is a fork of the library (it 18 | has since been archived). This allowed to to substantially reduce the surface 19 | we import. 20 | 21 | - `influxdb`: calls to objconv/json were replaced with 22 | github.com/segmentio/encoding/json (a library with 23 | substantially more production experience and test coverage). 24 | [#193](https://github.com/segmentio/stats/pull/193) 25 | 26 | - `prometheus`: Fix a deadlock from concurrent calls to collect() and cleanup(). 27 | Thank you Matthew Hooker for this contribution. 28 | [#194](https://github.com/segmentio/stats/pull/194) 29 | 30 | ### v5.5.0 (March 26, 2025) 31 | 32 | - Add logic to replace invalid unicode in the serialized datadog payload with '\ufffd'. 33 | 34 | ### v5.4.0 (February 21, 2025) 35 | 36 | - Fix a regression in configured buffer size for the datadog client. Versions 37 | 5.0.0 to 5.3.1 would ignore any configured buffer size and use the default value 38 | of 1024. This could lead to smaller than expected writes and contention for the 39 | file handle. 40 | 41 | ### v5.3.1 (January 2, 2025) 42 | 43 | - Fix version parsing logic. 44 | 45 | ### v5.3.0 (December 19, 2024) 46 | 47 | - Add `debugstats` package that can easily print metrics to stdout. 48 | 49 | - Fix file handle leak in procstats in the DelayMetrics collector. 50 | 51 | ### v5.2.0 52 | 53 | - `go_version.value` and `stats_version.value` will be emitted the first 54 | time any metrics are sent from an Engine. Disable this behavior by setting 55 | `GoVersionReportingEnabled = false` in code, or setting environment variable 56 | `STATS_DISABLE_GO_VERSION_REPORTING=true`. 57 | 58 | ### v5.1.0 59 | 60 | Add support for publishing stats to Unix datagram sockets (UDS). 61 | 62 | ### v5.0.0 (Released on September 11, 2024) 63 | 64 | In the `httpstats` package, replace misspelled `http_req_content_endoing` 65 | and `http_res_content_endoing` with `http_req_content_encoding` and 66 | `http_res_content_encoding`, respectively. This is a breaking change; any 67 | dashboards or queries that filter on this tag must be updated. 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Segment 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -trimpath ./... 3 | 4 | ci: 5 | go test -race -trimpath ./... 6 | 7 | lint: 8 | golangci-lint run --config .golangci.yml 9 | 10 | release: 11 | go run github.com/kevinburke/bump_version@latest --tag-prefix=v minor version/version.go 12 | 13 | force: ; 14 | 15 | AUTHORS.txt: force 16 | go run github.com/kevinburke/write_mailmap@latest > AUTHORS.txt 17 | 18 | authors: AUTHORS.txt 19 | 20 | bench: 21 | go test -bench . -run='^$$' -benchmem -count=10 ./... 22 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure("2") do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://vagrantcloud.com/search. 15 | config.vm.box = "ubuntu/xenial64" 16 | 17 | # Disable automatic box update checking. If you disable this, then 18 | # boxes will only be checked for updates when the user runs 19 | # `vagrant box outdated`. This is not recommended. 20 | # config.vm.box_check_update = false 21 | 22 | # Create a forwarded port mapping which allows access to a specific port 23 | # within the machine from a port on the host machine. In the example below, 24 | # accessing "localhost:8080" will access port 80 on the guest machine. 25 | # NOTE: This will enable public access to the opened port 26 | # config.vm.network "forwarded_port", guest: 80, host: 8080 27 | 28 | # Create a forwarded port mapping which allows access to a specific port 29 | # within the machine from a port on the host machine and only allow access 30 | # via 127.0.0.1 to disable public access 31 | # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1" 32 | 33 | # Create a private network, which allows host-only access to the machine 34 | # using a specific IP. 35 | # config.vm.network "private_network", ip: "192.168.33.10" 36 | 37 | # Create a public network, which generally matched to bridged network. 38 | # Bridged networks make the machine appear as another physical device on 39 | # your network. 40 | # config.vm.network "public_network" 41 | 42 | # Share an additional folder to the guest VM. The first argument is 43 | # the path on the host to the actual folder. The second argument is 44 | # the path on the guest to mount the folder. And the optional third 45 | # argument is a set of non-required options. 46 | config.vm.synced_folder ".", "/go/src/github.com/segmentio/stats" 47 | 48 | # Provider-specific configuration so you can fine-tune various 49 | # backing providers for Vagrant. These expose provider-specific options. 50 | # Example for VirtualBox: 51 | # 52 | # config.vm.provider "virtualbox" do |vb| 53 | # # Display the VirtualBox GUI when booting the machine 54 | # vb.gui = true 55 | # 56 | # # Customize the amount of memory on the VM: 57 | # vb.memory = "1024" 58 | # end 59 | # 60 | # View the documentation for the provider you are using for more 61 | # information on available options. 62 | 63 | # Enable provisioning with a shell script. Additional provisioners such as 64 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the 65 | # documentation for more information about their specific syntax and use. 66 | # config.vm.provision "shell", inline: <<-SHELL 67 | # apt-get update 68 | # apt-get install -y apache2 69 | # SHELL 70 | end 71 | -------------------------------------------------------------------------------- /buckets.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import "strings" 4 | 5 | // Key is a type used to uniquely identify metrics. 6 | type Key struct { 7 | Measure string 8 | Field string 9 | } 10 | 11 | // HistogramBuckets is a map type storing histogram buckets. 12 | type HistogramBuckets map[Key][]Value 13 | 14 | // Set sets a set of buckets to the given list of sorted values. 15 | func (b HistogramBuckets) Set(key string, buckets ...interface{}) { 16 | v := make([]Value, len(buckets)) 17 | 18 | for i, b := range buckets { 19 | v[i] = MustValueOf(ValueOf(b)) 20 | } 21 | 22 | b[makeKey(key)] = v 23 | } 24 | 25 | // Buckets is a registry where histogram buckets are placed. Some metric 26 | // collection backends need to have histogram buckets defined by the program 27 | // (like Prometheus), a common pattern is to use the init function of a package 28 | // to register buckets for the various histograms that it produces. 29 | var Buckets = HistogramBuckets{} 30 | 31 | func makeKey(s string) Key { 32 | measure, field := splitMeasureField(s) 33 | return Key{Measure: measure, Field: field} 34 | } 35 | 36 | func splitMeasureField(s string) (measure, field string) { 37 | if i := strings.LastIndexByte(s, '.'); i >= 0 { 38 | measure, field = s[:i], s[i+1:] 39 | } else { 40 | field = s 41 | } 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /clock.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import "time" 4 | 5 | // The Clock type can be used to report statistics on durations. 6 | // 7 | // Clocks are useful to measure the duration taken by sequential execution steps 8 | // and therefore aren't safe to be used concurrently by multiple goroutines. 9 | // 10 | // Note: Clock times are reported to datadog in seconds. See `stats/datadog/measure.go`. 11 | type Clock struct { 12 | name string 13 | first time.Time 14 | last time.Time 15 | tags []Tag 16 | eng *Engine 17 | } 18 | 19 | // Stamp reports the time difference between now and the last time the method 20 | // was called (or since the clock was created). 21 | // 22 | // The metric produced by this method call will have a "stamp" tag set to name. 23 | func (c *Clock) Stamp(name string) { 24 | c.StampAt(name, time.Now()) 25 | } 26 | 27 | // StampAt reports the time difference between now and the last time the method 28 | // was called (or since the clock was created). 29 | // 30 | // The metric produced by this method call will have a "stamp" tag set to name. 31 | func (c *Clock) StampAt(name string, now time.Time) { 32 | c.observe(name, now.Sub(c.last)) 33 | c.last = now 34 | } 35 | 36 | // Stop reports the time difference between now and the time the clock was created at. 37 | // 38 | // The metric produced by this method call will have a "stamp" tag set to 39 | // "total". 40 | func (c *Clock) Stop() { 41 | c.StopAt(time.Now()) 42 | } 43 | 44 | // StopAt reports the time difference between now and the time the clock was created at. 45 | // 46 | // The metric produced by this method call will have a "stamp" tag set to 47 | // "total". 48 | func (c *Clock) StopAt(now time.Time) { 49 | c.observe("total", now.Sub(c.first)) 50 | } 51 | 52 | func (c *Clock) observe(stamp string, d time.Duration) { 53 | tags := append(c.tags, Tag{"stamp", stamp}) 54 | c.eng.Observe(c.name, d, tags...) 55 | } 56 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // ContextWithTags returns a new child context with the given tags. If the 9 | // parent context already has tags set on it, they are _not_ propagated into 10 | // the context children. 11 | func ContextWithTags(ctx context.Context, tags ...Tag) context.Context { 12 | // initialize the context reference and return a new context 13 | return context.WithValue(ctx, contextKeyReqTags, &tagSlice{ 14 | tags: tags, 15 | }) 16 | } 17 | 18 | // ContextAddTags adds the given tags to the given context, if the tags have 19 | // been set on any of the ancestor contexts. ContextAddTags returns true 20 | // if tags were successfully appended to the context, and false otherwise. 21 | // 22 | // The proper way to set tags on a context if you don't know whether or not 23 | // tags already exist on the context is to first call ContextAddTags, and if 24 | // that returns false, then call ContextWithTags instead. 25 | func ContextAddTags(ctx context.Context, tags ...Tag) bool { 26 | if x := getTagSlice(ctx); x != nil { 27 | x.lock.Lock() 28 | x.tags = append(x.tags, tags...) 29 | x.lock.Unlock() 30 | return true 31 | } 32 | return false 33 | } 34 | 35 | // ContextTags returns a copy of the tags on the context if they exist and nil 36 | // if they don't exist. 37 | func ContextTags(ctx context.Context) []Tag { 38 | if x := getTagSlice(ctx); x != nil { 39 | x.lock.Lock() 40 | ret := make([]Tag, len(x.tags)) 41 | copy(ret, x.tags) 42 | x.lock.Unlock() 43 | return ret 44 | } 45 | return nil 46 | } 47 | 48 | func getTagSlice(ctx context.Context) *tagSlice { 49 | if tags, ok := ctx.Value(contextKeyReqTags).(*tagSlice); ok { 50 | return tags 51 | } 52 | return nil 53 | } 54 | 55 | type tagSlice struct { 56 | tags []Tag 57 | lock sync.Mutex 58 | } 59 | 60 | // tagsKey is a value for use with context.WithValue. It's used as 61 | // a pointer so it fits in an interface{} without allocation. This technique 62 | // for defining context keys was copied from Go 1.7's new use of context in net/http. 63 | type tagsKey struct{} 64 | 65 | // String implements the fmt.Stringer interface. 66 | func (k tagsKey) String() string { 67 | return "stats_tags_context_key" 68 | } 69 | 70 | // contextKeyReqTags is contextKey for tags. 71 | var ( 72 | contextKeyReqTags = tagsKey{} 73 | ) 74 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestContextTags(t *testing.T) { 11 | x := context.Background() 12 | 13 | y := ContextWithTags(x) 14 | assert.Equal(t, 0, len(ContextTags(y)), "Initialize context tags context value") 15 | ContextAddTags(y, T("asdf", "qwer")) 16 | assert.Equal(t, 1, len(ContextTags(y)), "Adding tags should result new tags") 17 | assert.Equal(t, 0, len(ContextTags(x)), "Original context should have no tags (because no context with key)") 18 | 19 | // create a child context which creates a child context 20 | type unimportant struct{} 21 | z := context.WithValue(y, unimportant{}, "important") 22 | assert.Equal(t, 1, len(ContextTags(z)), "We should still be able to see original tags") 23 | 24 | // Add tags to the child context's reference to the original tag slice 25 | ContextAddTags(z, T("zxcv", "uiop")) 26 | assert.Equal(t, 2, len(ContextTags(z)), "Updating tags should update local reference") 27 | assert.Equal(t, 2, len(ContextTags(y)), "Updating tags should update parent reference") 28 | assert.Equal(t, 0, len(ContextTags(x)), "Updating tags should not appear on original context") 29 | 30 | ContextAddTags(z, T("a", "k"), T("b", "k"), T("c", "k"), T("d", "k"), T("e", "k"), T("f", "k")) 31 | assert.Equal(t, 8, len(ContextTags(z)), "Updating tags should update local reference") 32 | assert.Equal(t, 8, len(ContextTags(y)), "Updating tags should update parent reference") 33 | assert.Equal(t, 0, len(ContextTags(x)), "Updating tags should not appear on original context") 34 | } 35 | -------------------------------------------------------------------------------- /datadog/append.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | ) 9 | 10 | func appendMetric(b []byte, m Metric) []byte { 11 | if len(m.Namespace) != 0 { 12 | b = append(b, m.Namespace...) 13 | b = append(b, '.') 14 | } 15 | 16 | b = append(b, m.Name...) 17 | b = append(b, ':') 18 | b = strconv.AppendFloat(b, m.Value, 'g', -1, 64) 19 | b = append(b, '|') 20 | b = append(b, m.Type...) 21 | 22 | if m.Rate != 0 && m.Rate != 1 { 23 | b = append(b, '|', '@') 24 | b = strconv.AppendFloat(b, m.Rate, 'g', -1, 64) 25 | } 26 | 27 | if n := len(m.Tags); n != 0 { 28 | b = append(b, '|', '#') 29 | b = appendTags(b, m.Tags) 30 | } 31 | 32 | return append(b, '\n') 33 | } 34 | 35 | func appendEvent(b []byte, e Event) []byte { 36 | b = append(b, '_', 'e', '{') 37 | b = strconv.AppendInt(b, int64(len(e.Title)), 10) 38 | b = append(b, ',') 39 | b = strconv.AppendInt(b, int64(len(e.Text)), 10) 40 | b = append(b, '}', ':') 41 | b = append(b, e.Title...) 42 | b = append(b, '|') 43 | 44 | b = append(b, strings.ReplaceAll(e.Text, "\n", "\\n")...) 45 | 46 | if e.Priority != EventPriorityNormal { 47 | b = append(b, '|', 'p', ':') 48 | b = append(b, e.Priority...) 49 | } 50 | 51 | if e.AlertType != EventAlertTypeInfo { 52 | b = append(b, '|', 't', ':') 53 | b = append(b, e.AlertType...) 54 | } 55 | 56 | if e.Ts != int64(0) { 57 | b = append(b, '|', 'd', ':') 58 | b = strconv.AppendInt(b, e.Ts, 10) 59 | } 60 | 61 | if len(e.Host) > 0 { 62 | b = append(b, '|', 'h', ':') 63 | b = append(b, e.Host...) 64 | } 65 | 66 | if len(e.AggregationKey) > 0 { 67 | b = append(b, '|', 'k', ':') 68 | b = append(b, e.AggregationKey...) 69 | } 70 | 71 | if len(e.SourceTypeName) > 0 { 72 | b = append(b, '|', 's', ':') 73 | b = append(b, e.SourceTypeName...) 74 | } 75 | 76 | if n := len(e.Tags); n != 0 { 77 | b = append(b, '|', '#') 78 | b = appendTags(b, e.Tags) 79 | } 80 | 81 | return append(b, '\n') 82 | } 83 | 84 | func appendTags(b []byte, tags []stats.Tag) []byte { 85 | for i, t := range tags { 86 | if i != 0 { 87 | b = append(b, ',') 88 | } 89 | 90 | b = append(b, t.Name...) 91 | b = append(b, ':') 92 | b = append(b, t.Value...) 93 | } 94 | return b 95 | } 96 | -------------------------------------------------------------------------------- /datadog/append_test.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import "testing" 4 | 5 | func TestAppendMetric(t *testing.T) { 6 | for _, test := range testMetrics { 7 | t.Run(test.m.Name, func(t *testing.T) { 8 | if s := string(appendMetric(nil, test.m)); s != test.s { 9 | t.Errorf("\n<<< %#v\n>>> %#v", test.s, s) 10 | } 11 | }) 12 | } 13 | } 14 | 15 | func BenchmarkAppendMetric(b *testing.B) { 16 | buffer := make([]byte, 4096) 17 | 18 | for _, test := range testMetrics { 19 | b.Run(test.m.Name, func(b *testing.B) { 20 | for i := 0; i != b.N; i++ { 21 | appendMetric(buffer[:0], test.m) 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /datadog/event.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | "fmt" 5 | 6 | stats "github.com/segmentio/stats/v5" 7 | ) 8 | 9 | // EventPriority is an enumeration providing the available datadog event 10 | // priority levels. 11 | type EventPriority string 12 | 13 | // Event Priorities. 14 | const ( 15 | EventPriorityNormal EventPriority = "normal" 16 | EventPriorityLow EventPriority = "low" 17 | ) 18 | 19 | // EventAlertType is an enumeration providing the available datadog event 20 | // allert types. 21 | type EventAlertType string 22 | 23 | // Event Alert Types. 24 | const ( 25 | EventAlertTypeError EventAlertType = "error" 26 | EventAlertTypeWarning EventAlertType = "warning" 27 | EventAlertTypeInfo EventAlertType = "info" 28 | EventAlertTypeSuccess EventAlertType = "success" 29 | ) 30 | 31 | // Event is a representation of a datadog event. 32 | type Event struct { 33 | Title string 34 | Text string 35 | Ts int64 36 | Priority EventPriority 37 | Host string 38 | Tags []stats.Tag 39 | AlertType EventAlertType 40 | AggregationKey string 41 | SourceTypeName string 42 | EventType string 43 | } 44 | 45 | // String satisfies the fmt.Stringer interface. 46 | func (e Event) String() string { 47 | return fmt.Sprint(e) 48 | } 49 | 50 | // Format satisfies the fmt.Formatter interface. 51 | func (e Event) Format(f fmt.State, _ rune) { 52 | buf := bufferPool.Get().(*buffer) 53 | buf.b = appendEvent(buf.b[:0], e) 54 | _, _ = f.Write(buf.b) 55 | bufferPool.Put(buf) 56 | } 57 | -------------------------------------------------------------------------------- /datadog/event_test.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | stats "github.com/segmentio/stats/v5" 5 | ) 6 | 7 | var testEvents = []struct { 8 | s string 9 | e Event 10 | }{ 11 | { 12 | s: "_e{10,9}:test title|test text\n", 13 | e: Event{ 14 | Title: "test title", 15 | Text: "test text", 16 | Priority: EventPriorityNormal, 17 | AlertType: EventAlertTypeInfo, 18 | }, 19 | }, 20 | { 21 | s: "_e{10,24}:test title|test\\line1\\nline2\\nline3\n", 22 | e: Event{ 23 | Title: "test title", 24 | Text: "test\\line1\nline2\nline3", 25 | Priority: EventPriorityNormal, 26 | AlertType: EventAlertTypeInfo, 27 | }, 28 | }, 29 | { 30 | s: "_e{10,24}:test|title|test\\line1\\nline2\\nline3\n", 31 | e: Event{ 32 | Title: "test|title", 33 | Text: "test\\line1\nline2\nline3", 34 | Priority: EventPriorityNormal, 35 | AlertType: EventAlertTypeInfo, 36 | }, 37 | }, 38 | { 39 | s: "_e{10,9}:test title|test text|d:21\n", 40 | e: Event{ 41 | Title: "test title", 42 | Text: "test text", 43 | Ts: int64(21), 44 | Priority: EventPriorityNormal, 45 | AlertType: EventAlertTypeInfo, 46 | }, 47 | }, 48 | { 49 | s: "_e{10,9}:test title|test text|p:low\n", 50 | e: Event{ 51 | Title: "test title", 52 | Text: "test text", 53 | Priority: EventPriorityLow, 54 | AlertType: EventAlertTypeInfo, 55 | }, 56 | }, 57 | { 58 | s: "_e{10,9}:test title|test text|h:localhost\n", 59 | e: Event{ 60 | Title: "test title", 61 | Text: "test text", 62 | Host: "localhost", 63 | Priority: EventPriorityNormal, 64 | AlertType: EventAlertTypeInfo, 65 | }, 66 | }, 67 | { 68 | s: "_e{10,9}:test title|test text|t:warning\n", 69 | e: Event{ 70 | Title: "test title", 71 | Text: "test text", 72 | Priority: EventPriorityNormal, 73 | AlertType: EventAlertTypeWarning, 74 | }, 75 | }, 76 | { 77 | s: "_e{10,9}:test title|test text|k:some aggregation key\n", 78 | e: Event{ 79 | Title: "test title", 80 | Text: "test text", 81 | AggregationKey: "some aggregation key", 82 | Priority: EventPriorityNormal, 83 | AlertType: EventAlertTypeInfo, 84 | }, 85 | }, 86 | { 87 | s: "_e{10,9}:test title|test text|s:this is the source\n", 88 | e: Event{ 89 | Title: "test title", 90 | Text: "test text", 91 | Priority: EventPriorityNormal, 92 | AlertType: EventAlertTypeInfo, 93 | SourceTypeName: "this is the source", 94 | }, 95 | }, 96 | { 97 | s: "_e{10,9}:test title|test text|#tag1,tag2:test\n", 98 | e: Event{ 99 | Title: "test title", 100 | Text: "test text", 101 | Priority: EventPriorityNormal, 102 | AlertType: EventAlertTypeInfo, 103 | Tags: []stats.Tag{ 104 | stats.T("tag1", ""), 105 | stats.T("tag2", "test"), 106 | }, 107 | }, 108 | }, 109 | } 110 | -------------------------------------------------------------------------------- /datadog/metric.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | ) 9 | 10 | // MetricType is an enumeration providing symbols to represent the different 11 | // metric types supported by datadog. 12 | type MetricType string 13 | 14 | // Metric Types. 15 | const ( 16 | Counter MetricType = "c" 17 | Gauge MetricType = "g" 18 | Histogram MetricType = "h" 19 | Distribution MetricType = "d" 20 | Unknown MetricType = "?" 21 | ) 22 | 23 | // The Metric type is a representation of the metrics supported by datadog. 24 | type Metric struct { 25 | Type MetricType // the metric type 26 | Namespace string // the metric namespace (never populated by parsing operations) 27 | Name string // the metric name 28 | Value float64 // the metric value 29 | Rate float64 // sample rate, a value between 0 and 1 30 | Tags []stats.Tag // the list of tags set on the metric 31 | } 32 | 33 | // String satisfies the fmt.Stringer interface. 34 | func (m Metric) String() string { 35 | return fmt.Sprint(m) 36 | } 37 | 38 | // Format satisfies the fmt.Formatter interface. 39 | func (m Metric) Format(f fmt.State, _ rune) { 40 | buf := bufferPool.Get().(*buffer) 41 | buf.b = appendMetric(buf.b[:0], m) 42 | _, _ = f.Write(buf.b) 43 | bufferPool.Put(buf) 44 | } 45 | 46 | type buffer struct { 47 | b []byte 48 | } 49 | 50 | var bufferPool = sync.Pool{ 51 | New: func() interface{} { return &buffer{make([]byte, 0, 512)} }, 52 | } 53 | -------------------------------------------------------------------------------- /datadog/metric_test.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | "testing" 5 | 6 | stats "github.com/segmentio/stats/v5" 7 | ) 8 | 9 | var testMetrics = []struct { 10 | s string 11 | m Metric 12 | }{ 13 | { 14 | s: "test.metric.small:0|c\n", 15 | m: Metric{ 16 | Type: Counter, 17 | Name: "test.metric.small", 18 | Tags: nil, 19 | Value: 0, 20 | Rate: 1, 21 | }, 22 | }, 23 | 24 | { 25 | s: "test.metric.common:1|c|#hello:world,answer:42\n", 26 | m: Metric{ 27 | Type: Counter, 28 | Name: "test.metric.common", 29 | Tags: []stats.Tag{ 30 | stats.T("hello", "world"), 31 | stats.T("answer", "42"), 32 | }, 33 | Value: 1, 34 | Rate: 1, 35 | }, 36 | }, 37 | 38 | { 39 | s: "test.metric.large:1.234|c|@0.1|#hello:world,hello:world,hello:world,hello:world,hello:world,hello:world,hello:world,hello:world,hello:world,hello:world\n", 40 | m: Metric{ 41 | Type: Counter, 42 | Name: "test.metric.large", 43 | Tags: []stats.Tag{ 44 | stats.T("hello", "world"), 45 | stats.T("hello", "world"), 46 | stats.T("hello", "world"), 47 | stats.T("hello", "world"), 48 | stats.T("hello", "world"), 49 | stats.T("hello", "world"), 50 | stats.T("hello", "world"), 51 | stats.T("hello", "world"), 52 | stats.T("hello", "world"), 53 | stats.T("hello", "world"), 54 | }, 55 | Value: 1.234, 56 | Rate: 0.1, 57 | }, 58 | }, 59 | 60 | { 61 | s: "page.views:1|c\n", 62 | m: Metric{ 63 | Type: Counter, 64 | Name: "page.views", 65 | Value: 1, 66 | Rate: 1, 67 | Tags: nil, 68 | }, 69 | }, 70 | 71 | { 72 | s: "fuel.level:0.5|g\n", 73 | m: Metric{ 74 | Type: Gauge, 75 | Name: "fuel.level", 76 | Value: 0.5, 77 | Rate: 1, 78 | Tags: nil, 79 | }, 80 | }, 81 | 82 | { 83 | s: "song.length:240|h|@0.5\n", 84 | m: Metric{ 85 | Type: Histogram, 86 | Name: "song.length", 87 | Value: 240, 88 | Rate: 0.5, 89 | Tags: nil, 90 | }, 91 | }, 92 | 93 | { 94 | s: "users.uniques:1234|h\n", 95 | m: Metric{ 96 | Type: Histogram, 97 | Name: "users.uniques", 98 | Value: 1234, 99 | Rate: 1, 100 | Tags: nil, 101 | }, 102 | }, 103 | 104 | { 105 | s: "song.length:240|d|@0.5\n", 106 | m: Metric{ 107 | Type: Distribution, 108 | Name: "song.length", 109 | Value: 240, 110 | Rate: 0.5, 111 | Tags: nil, 112 | }, 113 | }, 114 | 115 | { 116 | s: "users.uniques:1234|d\n", 117 | m: Metric{ 118 | Type: Distribution, 119 | Name: "users.uniques", 120 | Value: 1234, 121 | Rate: 1, 122 | Tags: nil, 123 | }, 124 | }, 125 | 126 | { 127 | s: "users.online:1|c|#country:china\n", 128 | m: Metric{ 129 | Type: Counter, 130 | Name: "users.online", 131 | Value: 1, 132 | Rate: 1, 133 | Tags: []stats.Tag{ 134 | stats.T("country", "china"), 135 | }, 136 | }, 137 | }, 138 | 139 | { 140 | s: "users.online:1|c|@0.5|#country:china\n", 141 | m: Metric{ 142 | Type: Counter, 143 | Name: "users.online", 144 | Value: 1, 145 | Rate: 0.5, 146 | Tags: []stats.Tag{ 147 | stats.T("country", "china"), 148 | }, 149 | }, 150 | }, 151 | } 152 | 153 | func TestMetricString(t *testing.T) { 154 | for _, test := range testMetrics { 155 | t.Run(test.s, func(t *testing.T) { 156 | if s := test.m.String(); s != test.s { 157 | t.Error(s) 158 | } 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /datadog/parse_test.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseMetricSuccess(t *testing.T) { 9 | for _, test := range testMetrics { 10 | t.Run(test.s, func(t *testing.T) { 11 | if m, err := parseMetric(test.s); err != nil { 12 | t.Error(err) 13 | } else if !reflect.DeepEqual(m, test.m) { 14 | t.Errorf("%#v:\n- %#v\n- %#v", test.s, test.m, m) 15 | } 16 | }) 17 | } 18 | } 19 | 20 | func TestParseMetricFailure(t *testing.T) { 21 | tests := []string{ 22 | "", 23 | ":10|c", // missing name 24 | "name:|c", // missing value 25 | "name:abc|c", // malformed value 26 | "name:1", // missing type 27 | "name:1|", // missing type 28 | "name:1|c|???", // malformed sample rate 29 | "name:1|c|@abc", // malformed sample rate 30 | "name:1|c|@0.5|???", // malformed tags 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test, func(t *testing.T) { 35 | if _, err := parseMetric(test); err == nil { 36 | t.Errorf("%#v: expected parsing error", test) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func BenchmarkParseMetric(b *testing.B) { 43 | for _, test := range testMetrics { 44 | b.Run(test.m.Name, func(b *testing.B) { 45 | for i := 0; i != b.N; i++ { 46 | parseMetric(test.s) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestParseEventSuccess(t *testing.T) { 53 | for _, test := range testEvents { 54 | t.Run(test.s, func(t *testing.T) { 55 | if e, err := parseEvent(test.s); err != nil { 56 | t.Error(err) 57 | } else if !reflect.DeepEqual(e, test.e) { 58 | t.Errorf("%#v:\n- %#v\n- %#v", test.s, test.e, e) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func BenchmarkParseEvent(b *testing.B) { 65 | for _, test := range testEvents { 66 | b.Run(test.e.Title, func(b *testing.B) { 67 | for i := 0; i != b.N; i++ { 68 | parseEvent(test.s) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /datadog/server.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net" 8 | "runtime" 9 | "time" 10 | 11 | "golang.org/x/sync/errgroup" 12 | ) 13 | 14 | // Handler defines the interface that types must satisfy to process metrics 15 | // received by a dogstatsd server. 16 | type Handler interface { 17 | // HandleMetric is called when a dogstatsd server receives a metric. 18 | // The method receives the metric and the address from which it was sent. 19 | HandleMetric(Metric, net.Addr) 20 | 21 | // HandleEvent is called when a dogstatsd server receives an event. 22 | // The method receives the metric and the address from which it was sent. 23 | HandleEvent(Event, net.Addr) 24 | } 25 | 26 | // HandlerFunc makes it possible for function types to be used as metric 27 | // handlers on dogstatsd servers. 28 | type HandlerFunc func(Metric, net.Addr) 29 | 30 | // HandleMetric calls f(m, a). 31 | func (f HandlerFunc) HandleMetric(m Metric, a net.Addr) { 32 | f(m, a) 33 | } 34 | 35 | // HandleEvent is a no-op for backwards compatibility. 36 | func (f HandlerFunc) HandleEvent(Event, net.Addr) {} 37 | 38 | // ListenAndServe starts a new dogstatsd server, listening for UDP datagrams on 39 | // addr and forwarding the metrics to handler. 40 | func ListenAndServe(addr string, handler Handler) (err error) { 41 | var conn net.PacketConn 42 | 43 | if conn, err = net.ListenPacket("udp", addr); err != nil { 44 | return 45 | } 46 | 47 | err = Serve(conn, handler) 48 | return 49 | } 50 | 51 | // Serve runs a dogstatsd server, listening for datagrams on conn and forwarding 52 | // the metrics to handler. 53 | func Serve(conn net.PacketConn, handler Handler) error { 54 | defer conn.Close() 55 | 56 | concurrency := runtime.GOMAXPROCS(-1) 57 | if concurrency <= 0 { 58 | concurrency = 1 59 | } 60 | 61 | err := conn.SetDeadline(time.Time{}) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | var errgrp errgroup.Group 67 | 68 | for i := 0; i < concurrency; i++ { 69 | errgrp.Go(func() error { 70 | return serve(conn, handler) 71 | }) 72 | } 73 | 74 | err = errgrp.Wait() 75 | switch { 76 | default: 77 | return err 78 | case err == nil: 79 | case errors.Is(err, io.EOF): 80 | case errors.Is(err, io.ErrClosedPipe): 81 | case errors.Is(err, io.ErrUnexpectedEOF): 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func serve(conn net.PacketConn, handler Handler) error { 88 | b := make([]byte, 65536) 89 | 90 | for { 91 | n, a, err := conn.ReadFrom(b) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | for s := b[:n]; len(s) != 0; { 97 | off := bytes.IndexByte(s, '\n') 98 | if off < 0 { 99 | off = len(s) 100 | } else { 101 | off++ 102 | } 103 | 104 | ln := s[:off] 105 | s = s[off:] 106 | 107 | if bytes.HasPrefix(ln, []byte("_e")) { 108 | e, err := parseEvent(string(ln)) 109 | if err != nil { 110 | continue 111 | } 112 | 113 | handler.HandleEvent(e, a) 114 | continue 115 | } 116 | 117 | m, err := parseMetric(string(ln)) 118 | if err != nil { 119 | continue 120 | } 121 | 122 | handler.HandleMetric(m, a) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /datadog/udp.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type udpWriter struct { 8 | conn net.Conn 9 | } 10 | 11 | // newUDPWriter returns a pointer to a new newUDPWriter given a socket file path as addr. 12 | func newUDPWriter(addr string) (*udpWriter, error) { 13 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 14 | if err != nil { 15 | return nil, err 16 | } 17 | conn, err := net.DialUDP("udp", nil, udpAddr) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return &udpWriter{conn: conn}, nil 22 | } 23 | 24 | // Write data to the UDP connection. 25 | func (w *udpWriter) Write(data []byte) (int, error) { 26 | return w.conn.Write(data) 27 | } 28 | 29 | func (w *udpWriter) Close() error { 30 | return w.conn.Close() 31 | } 32 | 33 | func (w *udpWriter) CalcBufferSize(sizehint int) (int, error) { 34 | f, err := w.conn.(*net.UDPConn).File() 35 | if err != nil { 36 | return 0, err 37 | } 38 | defer f.Close() 39 | 40 | return bufSizeFromFD(f, sizehint) 41 | } 42 | -------------------------------------------------------------------------------- /datadog/uds.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // UDSTimeout holds the default timeout for UDS socket writes, as they can get 10 | // blocking when the receiving buffer is full. 11 | // same value as in official datadog client: https://github.com/DataDog/datadog-go/blob/master/statsd/uds.go#L13 12 | const defaultUDSTimeout = 1 * time.Millisecond 13 | 14 | // udsWriter is an internal class wrapping around management of UDS connection 15 | // credits to Datadog team: https://github.com/DataDog/datadog-go/blob/master/statsd/uds.go 16 | type udsWriter struct { 17 | // Address to send metrics to, needed to allow reconnection on error 18 | addr net.Addr 19 | 20 | // Established connection object, or nil if not connected yet 21 | conn net.Conn 22 | connMu sync.RWMutex // so that we can replace the failing conn on error 23 | 24 | // write timeout 25 | writeTimeout time.Duration 26 | } 27 | 28 | // newUDSWriter returns a pointer to a new udsWriter given a socket file path as addr. 29 | func newUDSWriter(addr string) (*udsWriter, error) { 30 | udsAddr, err := net.ResolveUnixAddr("unixgram", addr) 31 | if err != nil { 32 | return nil, err 33 | } 34 | // Defer connection to first read/write 35 | writer := &udsWriter{addr: udsAddr, conn: nil, writeTimeout: defaultUDSTimeout} 36 | return writer, nil 37 | } 38 | 39 | // Write data to the UDS connection with write timeout and minimal error handling: 40 | // create the connection if nil, and destroy it if the statsd server has disconnected. 41 | func (w *udsWriter) Write(data []byte) (int, error) { 42 | conn, err := w.ensureConnection() 43 | if err != nil { 44 | return 0, err 45 | } 46 | 47 | if err = conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)); err != nil { 48 | return 0, err 49 | } 50 | 51 | n, err := conn.Write(data) 52 | // var netErr net.Error 53 | // if err != nil && (!errors.As(err, &netErr) || !err.()) { 54 | if err, isNetworkErr := err.(net.Error); err != nil && (!isNetworkErr || !err.Timeout()) { 55 | // Statsd server disconnected, retry connecting at next packet 56 | w.unsetConnection() 57 | return 0, err 58 | } 59 | return n, err 60 | } 61 | 62 | func (w *udsWriter) Close() error { 63 | if w.conn != nil { 64 | return w.conn.Close() 65 | } 66 | return nil 67 | } 68 | 69 | func (w *udsWriter) CalcBufferSize(sizehint int) (int, error) { 70 | conn, err := w.ensureConnection() 71 | if err != nil { 72 | return 0, err 73 | } 74 | f, err := conn.(*net.UnixConn).File() 75 | if err != nil { 76 | w.unsetConnection() 77 | return 0, err 78 | } 79 | defer f.Close() 80 | 81 | return bufSizeFromFD(f, sizehint) 82 | } 83 | 84 | func (w *udsWriter) ensureConnection() (net.Conn, error) { 85 | // Check if we've already got a socket we can use 86 | w.connMu.RLock() 87 | currentConn := w.conn 88 | w.connMu.RUnlock() 89 | 90 | if currentConn != nil { 91 | return currentConn, nil 92 | } 93 | 94 | // Looks like we might need to connect - try again with write locking. 95 | w.connMu.Lock() 96 | defer w.connMu.Unlock() 97 | if w.conn != nil { 98 | return w.conn, nil 99 | } 100 | 101 | newConn, err := net.Dial(w.addr.Network(), w.addr.String()) 102 | if err != nil { 103 | return nil, err 104 | } 105 | w.conn = newConn 106 | return newConn, nil 107 | } 108 | 109 | func (w *udsWriter) unsetConnection() { 110 | w.connMu.Lock() 111 | defer w.connMu.Unlock() 112 | w.conn = nil 113 | } 114 | -------------------------------------------------------------------------------- /datadog/uds_test.go: -------------------------------------------------------------------------------- 1 | package datadog 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestUDSReconnectsWhenConnRefused(t *testing.T) { 11 | dir, err := os.MkdirTemp("", "socket") 12 | if err != nil { 13 | t.Error(err) 14 | t.FailNow() 15 | } 16 | defer os.RemoveAll(dir) 17 | 18 | socketPath := filepath.Join(dir, "dsd.socket") 19 | closerServer1 := startUDSTestServerWithSocketFile(t, socketPath, HandlerFunc(func(_ Metric, _ net.Addr) {})) 20 | defer closerServer1.Close() 21 | 22 | client := NewClientWith(ClientConfig{ 23 | Address: "unixgram://" + socketPath, 24 | BufferSize: 1, // small buffer to force write to unix socket for each measure written 25 | }) 26 | 27 | measure := `main.http.error.count:0|c|#http_req_content_charset:,http_req_content_endoing:,http_req_content_type:,http_req_host:localhost:3011,http_req_method:GET,http_req_protocol:HTTP/1.1,http_req_transfer_encoding:identity 28 | ` 29 | 30 | _, err = client.Write([]byte(measure)) 31 | if err != nil { 32 | t.Errorf("unable to write data %v", err) 33 | } 34 | 35 | closerServer1.Close() 36 | 37 | _, err = client.Write([]byte(measure)) 38 | if err == nil { 39 | t.Errorf("got no error but expected one as the connection should be refused as we closed the server") 40 | } 41 | // restart UDS server with same socket file 42 | closerServer2 := startUDSTestServerWithSocketFile(t, socketPath, HandlerFunc(func(_ Metric, _ net.Addr) {})) 43 | defer closerServer2.Close() 44 | 45 | _, err = client.Write([]byte(measure)) 46 | if err != nil { 47 | t.Errorf("unable to write data but should be able to as the client should reconnect %v", err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /debugstats/debugstats.go: -------------------------------------------------------------------------------- 1 | // Package debugstats simplifies metric troubleshooting by sending metrics to 2 | // any io.Writer. 3 | // 4 | // By default, metrics will be printed to os.Stdout. Use the Dst and Grep fields 5 | // to customize the output as appropriate. 6 | package debugstats 7 | 8 | import ( 9 | "fmt" 10 | "io" 11 | "math" 12 | "os" 13 | "regexp" 14 | "strconv" 15 | "time" 16 | 17 | "github.com/segmentio/stats/v5" 18 | ) 19 | 20 | // Client will print out received metrics. If Dst is nil, metrics will be 21 | // printed to stdout, otherwise they will be printed to Dst. 22 | // 23 | // You can optionally provide a Grep regexp to limit printed metrics to ones 24 | // matching the regular expression. 25 | type Client struct { 26 | Dst io.Writer 27 | Grep *regexp.Regexp 28 | } 29 | 30 | func (c *Client) Write(p []byte) (int, error) { 31 | if c.Dst == nil { 32 | return os.Stdout.Write(p) 33 | } 34 | return c.Dst.Write(p) 35 | } 36 | 37 | func normalizeFloat(f float64) float64 { 38 | switch { 39 | case math.IsNaN(f): 40 | return 0.0 41 | case math.IsInf(f, +1): 42 | return +math.MaxFloat64 43 | case math.IsInf(f, -1): 44 | return -math.MaxFloat64 45 | default: 46 | return f 47 | } 48 | } 49 | 50 | func appendMeasure(b []byte, m stats.Measure) []byte { 51 | for _, field := range m.Fields { 52 | b = append(b, m.Name...) 53 | if len(field.Name) != 0 { 54 | b = append(b, '.') 55 | b = append(b, field.Name...) 56 | } 57 | b = append(b, ':') 58 | 59 | switch v := field.Value; v.Type() { 60 | case stats.Bool: 61 | if v.Bool() { 62 | b = append(b, '1') 63 | } else { 64 | b = append(b, '0') 65 | } 66 | case stats.Int: 67 | b = strconv.AppendInt(b, v.Int(), 10) 68 | case stats.Uint: 69 | b = strconv.AppendUint(b, v.Uint(), 10) 70 | case stats.Float: 71 | b = strconv.AppendFloat(b, normalizeFloat(v.Float()), 'g', -1, 64) 72 | case stats.Duration: 73 | b = strconv.AppendFloat(b, v.Duration().Seconds(), 'g', -1, 64) 74 | default: 75 | b = append(b, '0') 76 | } 77 | 78 | switch field.Type() { 79 | case stats.Counter: 80 | b = append(b, '|', 'c') 81 | case stats.Gauge: 82 | b = append(b, '|', 'g') 83 | default: 84 | b = append(b, '|', 'd') 85 | } 86 | 87 | if n := len(m.Tags); n != 0 { 88 | b = append(b, '|', '#') 89 | 90 | for i, t := range m.Tags { 91 | if i != 0 { 92 | b = append(b, ',') 93 | } 94 | b = append(b, t.Name...) 95 | b = append(b, ':') 96 | b = append(b, t.Value...) 97 | } 98 | } 99 | 100 | b = append(b, '\n') 101 | } 102 | 103 | return b 104 | } 105 | 106 | func (c *Client) HandleMeasures(t time.Time, measures ...stats.Measure) { 107 | for i := range measures { 108 | m := &measures[i] 109 | 110 | // Process and output the measure 111 | out := make([]byte, 0) 112 | out = appendMeasure(out, *m) 113 | if c.Grep != nil && !c.Grep.Match(out) { 114 | continue // Skip this measure 115 | } 116 | 117 | fmt.Fprintf(c, "%s %s", t.Format(time.RFC3339), out) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /debugstats/debugstats_test.go: -------------------------------------------------------------------------------- 1 | package debugstats 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/segmentio/stats/v5" 10 | ) 11 | 12 | func TestStdout(t *testing.T) { 13 | var buf bytes.Buffer 14 | s := &Client{Dst: &buf} 15 | stats.Register(s) 16 | stats.Set("blah", 7) 17 | stats.Observe("compression_ratio", 0.3, stats.T("file_size_bucket", "bucket_name"), stats.T("algorithm", "jwt256")) 18 | bufstr := buf.String() 19 | want := "debugstats.test.compression_ratio:0.3|d|#algorithm:jwt256,file_size_bucket:bucket_name\n" 20 | if !strings.HasSuffix(bufstr, want) { 21 | t.Errorf("debugstats: got %v want %v", bufstr, want) 22 | } 23 | } 24 | 25 | func TestStdoutGrepMatch(t *testing.T) { 26 | var buf bytes.Buffer 27 | s := &Client{ 28 | Dst: &buf, 29 | Grep: regexp.MustCompile(`compression_ratio`), 30 | } 31 | eng := stats.NewEngine("prefix", s) 32 | 33 | // Send measures that match and don't match the Grep pattern 34 | eng.Set("compression_ratio", 0.3) 35 | eng.Set("other_metric", 42) 36 | eng.Flush() 37 | 38 | bufstr := buf.String() 39 | 40 | // Check that only the matching measure is output 41 | if !strings.Contains(bufstr, "compression_ratio:0.3") { 42 | t.Errorf("debugstats: expected output to contain 'compression_ratio:0.3', but it did not. Output: %s", bufstr) 43 | } 44 | 45 | if strings.Contains(bufstr, "other_metric") { 46 | t.Errorf("debugstats: expected output not to contain 'other_metric', but it did. Output: %s", bufstr) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package stats exposes tools for producing application performance metrics 2 | // to various metric collection backends. 3 | package stats 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | influxdb: 3 | image: influxdb:alpine 4 | ports: 5 | - 8086:8086 6 | 7 | # If you are on arm64 and experiencing issues with the tests (hangs, 8 | # connection reset) then try the following in order: 9 | 10 | # - stopping and removing all downloaded container images 11 | # - ensuring you have the latest Docker Desktop version 12 | # - factory reset your Docker Desktop settings 13 | 14 | # If you are still running into issues please post in #help-infra-seg. 15 | platform: linux/amd64 16 | otel-collector: 17 | image: otel/opentelemetry-collector:0.48.0 18 | command: 19 | - "/otelcol" 20 | - "--config=/etc/otel-config.yaml" 21 | ports: 22 | - 4317:4317 23 | - 4318:4318 24 | - 4319:4319 25 | - 8888:8888 26 | volumes: 27 | - "./.otel/config.yaml:/etc/otel-config.yaml" 28 | # See platform comment above for amd64/arm64 troubleshooting 29 | platform: linux/amd64 30 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package stats_test 2 | 3 | import ( 4 | "os" 5 | 6 | stats "github.com/segmentio/stats/v5" 7 | "github.com/segmentio/stats/v5/debugstats" 8 | ) 9 | 10 | func Example() { 11 | handler := &debugstats.Client{Dst: os.Stdout} 12 | engine := stats.NewEngine("engine-name", handler) 13 | // Will print: 14 | // 15 | // 2024-12-18T14:53:57-08:00 engine-name.server.start:1|c 16 | // 17 | // to the console. 18 | engine.Incr("server.start") 19 | engine.Flush() 20 | } 21 | -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import "strconv" 4 | 5 | // A Field is a key/value type that represents a single metric in a Measure. 6 | type Field struct { 7 | Name string 8 | Value Value 9 | } 10 | 11 | // MakeField constructs and returns a new Field from name, value, and ftype. 12 | func MakeField(name string, value interface{}, ftype FieldType) Field { 13 | f := Field{Name: name, Value: MustValueOf(ValueOf(value))} 14 | f.setType(ftype) 15 | return f 16 | } 17 | 18 | // Type returns the type of f. 19 | func (f Field) Type() FieldType { 20 | return FieldType(f.Value.pad) 21 | } 22 | 23 | func (f *Field) setType(t FieldType) { 24 | // We pack the field type into the value's padding space to make copies and 25 | // assignments of fields more time efficient. 26 | // Here are the results of a microbenchmark showing the performance of 27 | // a simple assignment for a Field type of 40 bytes (with a Type field) vs 28 | // an assignment of a Tag type (32 bytes). 29 | // 30 | // $ go test -v -bench . -run _ 31 | // BenchmarkAssign40BytesStruct-4 1000000000 2.20 ns/op 32 | // BenchmarkAssign32BytesStruct-4 2000000000 0.31 ns/op 33 | // 34 | // There's an order of magnitude difference, so the optimization is worth it. 35 | f.Value.pad = int32(t) 36 | } 37 | 38 | func (f Field) String() string { 39 | return f.Type().String() + ":" + f.Name + "=" + f.Value.String() 40 | } 41 | 42 | // FieldType is an enumeration of the different metric types that may be set on 43 | // a Field value. 44 | type FieldType int32 45 | 46 | const ( 47 | // Counter represents incrementing counter metrics. 48 | Counter FieldType = iota 49 | 50 | // Gauge represents metrics that snapshot a value that may increase and 51 | // decrease. 52 | Gauge 53 | 54 | // Histogram represents metrics to observe the distribution of values. 55 | Histogram 56 | ) 57 | 58 | func (t FieldType) String() string { 59 | switch t { 60 | case Counter: 61 | return "counter" 62 | case Gauge: 63 | return "gauge" 64 | case Histogram: 65 | return "histogram" 66 | } 67 | return "" 68 | } 69 | 70 | // GoString return a string representation of the FieldType. 71 | func (t FieldType) GoString() string { 72 | switch t { 73 | case Counter: 74 | return "stats.Counter" 75 | case Gauge: 76 | return "stats.Gauge" 77 | case Histogram: 78 | return "stats.Histogram" 79 | default: 80 | return "stats.FieldType(" + strconv.Itoa(int(t)) + ")" 81 | } 82 | } 83 | 84 | func copyFields(fields []Field) []Field { 85 | if len(fields) == 0 { 86 | return nil 87 | } 88 | cfields := make([]Field, len(fields)) 89 | copy(cfields, fields) 90 | return cfields 91 | } 92 | -------------------------------------------------------------------------------- /field_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "testing" 5 | "unsafe" 6 | ) 7 | 8 | func TestFieldSize(t *testing.T) { 9 | size := unsafe.Sizeof(Field{}) 10 | t.Log("field size:", size) 11 | } 12 | 13 | func BenchmarkAssign40BytesStruct(b *testing.B) { 14 | type S struct { 15 | a string 16 | b string 17 | c int 18 | } 19 | 20 | var s S 21 | 22 | for i := 0; i != b.N; i++ { 23 | s = S{a: "hello", b: "", c: 0} 24 | _ = s 25 | } 26 | } 27 | 28 | func BenchmarkAssign32BytesStruct(b *testing.B) { 29 | type S struct { 30 | a string 31 | b string 32 | } 33 | 34 | var s S 35 | 36 | for i := 0; i != b.N; i++ { 37 | s = S{a: "hello", b: ""} 38 | _ = s 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/segmentio/stats/v5 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/mdlayher/taskstats v0.0.0-20241219020249-a291fa5f5a69 7 | github.com/segmentio/encoding v0.4.1 8 | github.com/segmentio/fasthash v1.0.3 9 | github.com/segmentio/vpcinfo v0.2.0 10 | github.com/stretchr/testify v1.10.0 11 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 12 | golang.org/x/sync v0.14.0 13 | golang.org/x/sys v0.33.0 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/google/go-cmp v0.7.0 // indirect 19 | github.com/josharian/native v1.1.0 // indirect 20 | github.com/kr/pretty v0.3.1 // indirect 21 | github.com/mdlayher/genetlink v1.3.2 // indirect 22 | github.com/mdlayher/netlink v1.7.2 // indirect 23 | github.com/mdlayher/socket v0.5.1 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | github.com/segmentio/asm v1.1.3 // indirect 26 | golang.org/x/net v0.40.0 // indirect 27 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | 31 | // this version contains an error that truncates metric, tag, and field names 32 | // once a buffer exceeds 250 characters 33 | retract v5.6.0 34 | -------------------------------------------------------------------------------- /grafana/annotations_test.go: -------------------------------------------------------------------------------- 1 | package grafana 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestAnnotationsHandler(t *testing.T) { 15 | t0 := time.Date(2017, 8, 16, 12, 34, 0, 0, time.UTC) 16 | t1 := t0.Add(1 * time.Minute) 17 | 18 | ar := annotationsRequest{} 19 | ar.Range.From = t0 20 | ar.Range.To = t1 21 | ar.Annotation.Name = "name" 22 | ar.Annotation.Datasource = "test" 23 | ar.Annotation.IconColor = "rgba(255, 96, 96, 1)" 24 | ar.Annotation.Query = "events" 25 | ar.Annotation.Enable = true 26 | 27 | b, _ := json.Marshal(ar) 28 | 29 | client := http.Client{} 30 | server := httptest.NewServer(NewAnnotationsHandler( 31 | AnnotationsHandlerFunc(func(_ context.Context, res AnnotationsResponse, req *AnnotationsRequest) error { 32 | if !req.From.Equal(ar.Range.From) { 33 | t.Error("bad 'from' time:", req.From, ar.Range.From) 34 | } 35 | 36 | if !req.To.Equal(ar.Range.To) { 37 | t.Error("bad 'to' time:", req.To, ar.Range.To) 38 | } 39 | 40 | if req.Name != ar.Annotation.Name { 41 | t.Error("bad name:", req.Name, "!=", ar.Annotation.Name) 42 | } 43 | 44 | if req.Datasource != ar.Annotation.Datasource { 45 | t.Error("bad datasource:", req.Datasource, "!=", ar.Annotation.Datasource) 46 | } 47 | 48 | if req.IconColor != ar.Annotation.IconColor { 49 | t.Error("bad icon color:", req.IconColor, "!=", ar.Annotation.IconColor) 50 | } 51 | 52 | if req.Query != ar.Annotation.Query { 53 | t.Error("bad query:", req.Query, "!=", ar.Annotation.Query) 54 | } 55 | 56 | if req.Enable != ar.Annotation.Enable { 57 | t.Error("not enabled:", req.Enable, "!=", ar.Annotation.Enable) 58 | } 59 | 60 | res.WriteAnnotation(Annotation{ 61 | Time: t0, 62 | Title: "yay!", 63 | Text: "we did it!", 64 | Enabled: true, 65 | ShowLine: true, 66 | Tags: []string{"A", "B", "C"}, 67 | }) 68 | 69 | return nil 70 | }), 71 | )) 72 | defer server.Close() 73 | 74 | req, _ := http.NewRequest("POST", server.URL+"/annotations?pretty", bytes.NewReader(b)) 75 | 76 | r, err := client.Do(req) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | defer r.Body.Close() 81 | 82 | found, _ := io.ReadAll(r.Body) 83 | expect := annotationsResult 84 | 85 | if s := string(found); s != expect { 86 | t.Error(s) 87 | t.Log(expect) 88 | } 89 | } 90 | 91 | const annotationsResult = `[ 92 | { 93 | "annotation": { 94 | "name": "name", 95 | "datasource": "test", 96 | "enabled": true, 97 | "showLine": true 98 | }, 99 | "time": 1502886840000, 100 | "title": "yay!", 101 | "text": "we did it!", 102 | "tags": "A, B, C" 103 | } 104 | ]` 105 | -------------------------------------------------------------------------------- /grafana/grafanatest/annotations.go: -------------------------------------------------------------------------------- 1 | package grafanatest 2 | 3 | import "github.com/segmentio/stats/v5/grafana" 4 | 5 | // AnnotationsResponse is an implementation of the grafana.AnnotationsResponse 6 | // interface which captures the values passed to its method calls. 7 | type AnnotationsResponse struct { 8 | Annotations []grafana.Annotation 9 | } 10 | 11 | // WriteAnnotation satisfies the grafana.AnnotationsResponse interface. 12 | func (res *AnnotationsResponse) WriteAnnotation(a grafana.Annotation) { 13 | res.Annotations = append(res.Annotations, a) 14 | } 15 | -------------------------------------------------------------------------------- /grafana/grafanatest/annotations_test.go: -------------------------------------------------------------------------------- 1 | package grafanatest 2 | 3 | import "github.com/segmentio/stats/v5/grafana" 4 | 5 | var _ grafana.AnnotationsResponse = (*AnnotationsResponse)(nil) 6 | -------------------------------------------------------------------------------- /grafana/grafanatest/query.go: -------------------------------------------------------------------------------- 1 | package grafanatest 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/segmentio/stats/v5/grafana" 7 | ) 8 | 9 | // QueryResponse is an implementation of the grafana.QueryResponse interface 10 | // which captures the values passed to its method calls. 11 | type QueryResponse struct { 12 | // Results is a list of values which are either of type Timeserie or Table. 13 | Results []interface{} 14 | } 15 | 16 | // Timeserie satisfies the grafana.QueryResponse interface. 17 | func (res *QueryResponse) Timeserie(target string) grafana.TimeserieWriter { 18 | t := &Timeserie{Target: target} 19 | res.Results = append(res.Results, t) 20 | return t 21 | } 22 | 23 | // Table satisfies the grafana.QueryResponse interface. 24 | func (res *QueryResponse) Table(columns ...grafana.Column) grafana.TableWriter { 25 | t := &Table{Columns: append(make([]grafana.Column, 0, len(columns)), columns...)} 26 | res.Results = append(res.Results, t) 27 | return t 28 | } 29 | 30 | // Timeserie values are used by a QueryResponse to capture responses to 31 | // timeserie queries. 32 | type Timeserie struct { 33 | Target string 34 | Values []float64 35 | Times []time.Time 36 | } 37 | 38 | // WriteDatapoint satisfies the grafana.TimeserieWriter interface. 39 | func (t *Timeserie) WriteDatapoint(value float64, time time.Time) { 40 | t.Values = append(t.Values, value) 41 | t.Times = append(t.Times, time) 42 | } 43 | 44 | // Table values are used by a QueryResponse to capture responses to table 45 | // queries. 46 | type Table struct { 47 | Columns []grafana.Column 48 | Rows [][]interface{} 49 | } 50 | 51 | // WriteRow satisfies the grafana.TableWriter interface. 52 | func (t *Table) WriteRow(values ...interface{}) { 53 | t.Rows = append(t.Rows, 54 | append(make([]interface{}, 0, len(values)), values...), 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /grafana/grafanatest/query_test.go: -------------------------------------------------------------------------------- 1 | package grafanatest 2 | 3 | import "github.com/segmentio/stats/v5/grafana" 4 | 5 | var _ grafana.QueryResponse = (*QueryResponse)(nil) 6 | -------------------------------------------------------------------------------- /grafana/grafanatest/search.go: -------------------------------------------------------------------------------- 1 | package grafanatest 2 | 3 | // SearchResponse is an implementation of the grafana.SearchResponse interface 4 | // which captures the values passed to its method calls. 5 | type SearchResponse struct { 6 | Targets []string 7 | Values []interface{} 8 | } 9 | 10 | // WriteTarget satisfies the grafana.SearchResponse interface. 11 | func (res *SearchResponse) WriteTarget(target string) { 12 | res.WriteTargetValue(target, nil) 13 | } 14 | 15 | // WriteTargetValue satisfies the grafana.SearchResponse interface. 16 | func (res *SearchResponse) WriteTargetValue(target string, value interface{}) { 17 | res.Targets = append(res.Targets, target) 18 | res.Values = append(res.Values, value) 19 | } 20 | -------------------------------------------------------------------------------- /grafana/grafanatest/search_test.go: -------------------------------------------------------------------------------- 1 | package grafanatest 2 | 3 | import "github.com/segmentio/stats/v5/grafana" 4 | 5 | var _ grafana.SearchResponse = (*SearchResponse)(nil) 6 | -------------------------------------------------------------------------------- /grafana/handler.go: -------------------------------------------------------------------------------- 1 | package grafana 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "path" 10 | 11 | "github.com/segmentio/stats/v5/util/objconv" 12 | "github.com/segmentio/stats/v5/util/objconv/json" 13 | ) 14 | 15 | // Handler is an interface that must be implemented by types that intend to act 16 | // as a grafana data source using the simple-json-datasource plugin. 17 | // 18 | // See https://github.com/grafana/simple-json-datasource for more details about 19 | // the implementation. 20 | type Handler interface { 21 | AnnotationsHandler 22 | QueryHandler 23 | SearchHandler 24 | } 25 | 26 | // NewHandler returns a new http.Handler that implements the 27 | // simple-json-datasource API. 28 | func NewHandler(prefix string, handler Handler) http.Handler { 29 | mux := http.NewServeMux() 30 | Handle(mux, prefix, handler) 31 | return mux 32 | } 33 | 34 | // Handle installs a handler implementing the simple-json-datasource API on mux. 35 | // 36 | // The function adds three routes to mux, for /annotations, /query, and /search. 37 | func Handle(mux *http.ServeMux, prefix string, handler Handler) { 38 | HandleAnnotations(mux, prefix, handler) 39 | HandleQuery(mux, prefix, handler) 40 | HandleSearch(mux, prefix, handler) 41 | 42 | // Registering a global handler is a common thing that applications do, to 43 | // avoid overriding one that may already exist we first check that none were 44 | // previously registered. 45 | root := path.Join("/", prefix) 46 | 47 | if _, pattern := mux.Handler(&http.Request{ 48 | URL: &url.URL{Path: root}, 49 | }); len(pattern) == 0 { 50 | mux.HandleFunc(root, func(res http.ResponseWriter, _ *http.Request) { 51 | setResponseHeaders(res) 52 | }) 53 | } 54 | } 55 | 56 | func handlerFunc(f func(context.Context, *objconv.StreamEncoder, *objconv.Decoder) error) http.Handler { 57 | return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 58 | setResponseHeaders(res) 59 | 60 | switch req.Method { 61 | case http.MethodPost: 62 | case http.MethodOptions: 63 | res.WriteHeader(http.StatusOK) 64 | return 65 | default: 66 | res.WriteHeader(http.StatusMethodNotAllowed) 67 | return 68 | } 69 | 70 | err := f( 71 | req.Context(), 72 | newEncoder(res, req), 73 | newDecoder(req.Body), 74 | ) 75 | 76 | // TODO: support different error types to return different error codes? 77 | switch err { 78 | case nil: 79 | case context.Canceled: 80 | res.WriteHeader(http.StatusInternalServerError) 81 | case context.DeadlineExceeded: 82 | res.WriteHeader(http.StatusGatewayTimeout) 83 | default: 84 | res.WriteHeader(http.StatusInternalServerError) 85 | log.Printf("grafana: %s %s: %s", req.Method, req.URL.Path, err) 86 | } 87 | }) 88 | } 89 | 90 | func newEncoder(res http.ResponseWriter, req *http.Request) *objconv.StreamEncoder { 91 | q := req.URL.Query() 92 | if _, ok := q["pretty"]; ok { 93 | return json.NewPrettyStreamEncoder(res) 94 | } 95 | return json.NewStreamEncoder(res) 96 | } 97 | 98 | func newDecoder(r io.Reader) *objconv.Decoder { 99 | return json.NewDecoder(r) 100 | } 101 | 102 | func setResponseHeaders(res http.ResponseWriter) { 103 | h := res.Header() 104 | h.Set("Access-Control-Allow-Headers", "Accept, Content-Type") 105 | h.Set("Access-Control-Allow-Methods", "POST") 106 | h.Set("Access-Control-Allow-Origin", "*") 107 | h.Set("Content-Type", "application/json; charset=utf-8") 108 | h.Set("Server", "stats/grafana (simple-json-datasource)") 109 | } 110 | -------------------------------------------------------------------------------- /grafana/search.go: -------------------------------------------------------------------------------- 1 | package grafana 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "path" 7 | 8 | "github.com/segmentio/stats/v5/util/objconv" 9 | ) 10 | 11 | // SearchHandler is the handler for the /search endpoint in the 12 | // simple-json-datasource API. 13 | type SearchHandler interface { 14 | // ServeSearch is expected to implement the search functionality of a 15 | // Grafana data source. 16 | // 17 | // Note: It's not really clear how search is implemented, I think the 18 | // "target" field in the request is some kind of prefix or keyword to 19 | // use to return a list of potential matches that can be used in a /query 20 | // request. 21 | ServeSearch(ctx context.Context, res SearchResponse, req *SearchRequest) error 22 | } 23 | 24 | // SearchHandlerFunc makes it possible to use regular function types as search 25 | // handlers. 26 | type SearchHandlerFunc func(context.Context, SearchResponse, *SearchRequest) error 27 | 28 | // ServeSearch calls f, satisfies the SearchHandler interface. 29 | func (f SearchHandlerFunc) ServeSearch(ctx context.Context, res SearchResponse, req *SearchRequest) error { 30 | return f(ctx, res, req) 31 | } 32 | 33 | // SearchResponse is an interface used to respond to a search request. 34 | type SearchResponse interface { 35 | // WriteTarget writes target in the response, the method may be called 36 | // multiple times. 37 | WriteTarget(target string) 38 | 39 | // WriteTargetValue writes the pair of target and value in the response, 40 | // the method may be called multiple times. 41 | WriteTargetValue(target string, value interface{}) 42 | } 43 | 44 | // SearchRequest represents a request received on the /search endpoint. 45 | type SearchRequest struct { 46 | Target string 47 | } 48 | 49 | // NewSearchHandler returns a new http.Handler which delegates /search API calls 50 | // to the given search handler. 51 | func NewSearchHandler(handler SearchHandler) http.Handler { 52 | return handlerFunc(func(ctx context.Context, enc *objconv.StreamEncoder, dec *objconv.Decoder) error { 53 | req := searchRequest{} 54 | res := searchResponse{enc: enc} 55 | 56 | if err := dec.Decode(&req); err != nil { 57 | return err 58 | } 59 | 60 | if err := handler.ServeSearch(ctx, &res, &SearchRequest{ 61 | Target: req.Target, 62 | }); err != nil { 63 | return err 64 | } 65 | 66 | return enc.Close() 67 | }) 68 | } 69 | 70 | // HandleSearch installs a handler on /search. 71 | func HandleSearch(mux *http.ServeMux, prefix string, handler SearchHandler) { 72 | mux.Handle(path.Join("/", prefix, "search"), NewSearchHandler(handler)) 73 | } 74 | 75 | type searchRequest struct { 76 | Target string `json:"target"` 77 | } 78 | 79 | type searchResponse struct { 80 | enc *objconv.StreamEncoder 81 | } 82 | 83 | func (res *searchResponse) WriteTarget(target string) { 84 | _ = res.enc.Encode(target) 85 | } 86 | 87 | func (res *searchResponse) WriteTargetValue(target string, value interface{}) { 88 | _ = res.enc.Encode(struct { 89 | Target string `json:"target"` 90 | Value interface{} `json:"value"` 91 | }{target, value}) 92 | } 93 | -------------------------------------------------------------------------------- /grafana/search_test.go: -------------------------------------------------------------------------------- 1 | package grafana 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | ) 12 | 13 | func TestSearchHandler(t *testing.T) { 14 | sr := searchRequest{ 15 | Target: "upper_50", 16 | } 17 | 18 | b, _ := json.Marshal(sr) 19 | 20 | client := http.Client{} 21 | server := httptest.NewServer(NewSearchHandler( 22 | SearchHandlerFunc(func(_ context.Context, res SearchResponse, req *SearchRequest) error { 23 | if req.Target != sr.Target { 24 | t.Error("bad 'from' time:", req.Target, "!=", sr.Target) 25 | } 26 | 27 | res.WriteTarget("upper_25") 28 | res.WriteTarget("upper_50") 29 | res.WriteTarget("upper_90") 30 | 31 | res.WriteTargetValue("upper_25", 1) 32 | res.WriteTargetValue("upper_50", 2) 33 | 34 | return nil 35 | }), 36 | )) 37 | defer server.Close() 38 | 39 | req, _ := http.NewRequest("POST", server.URL+"/search?pretty", bytes.NewReader(b)) 40 | 41 | r, err := client.Do(req) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | defer r.Body.Close() 46 | 47 | found, _ := io.ReadAll(r.Body) 48 | expect := searchResult 49 | 50 | if s := string(found); s != expect { 51 | t.Error(s) 52 | t.Log(expect) 53 | } 54 | } 55 | 56 | const searchResult = `[ 57 | "upper_25", 58 | "upper_50", 59 | "upper_90", 60 | { 61 | "target": "upper_25", 62 | "value": 1 63 | }, 64 | { 65 | "target": "upper_50", 66 | "value": 2 67 | } 68 | ]` 69 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import "time" 4 | 5 | // The Handler interface is implemented by types that produce measures to 6 | // various metric collection backends. 7 | type Handler interface { 8 | // HandleMeasures is called by the Engine on which the handler was set 9 | // whenever new measures are produced by the program. The first argument 10 | // is the time at which the measures were taken. 11 | // 12 | // The method must treat the list of measures as read-only values, and 13 | // must not retain pointers to any of the measures or their sub-fields 14 | // after returning. 15 | HandleMeasures(time time.Time, measures ...Measure) 16 | } 17 | 18 | // Flusher is an interface implemented by measure handlers in order to flush 19 | // any buffered data. 20 | type Flusher interface { 21 | Flush() 22 | } 23 | 24 | func flush(h Handler) { 25 | if f, ok := h.(Flusher); ok { 26 | f.Flush() 27 | } 28 | } 29 | 30 | // HandlerFunc is a type alias making it possible to use simple functions as 31 | // measure handlers. 32 | type HandlerFunc func(time.Time, ...Measure) 33 | 34 | // HandleMeasures calls f, satisfies the Handler interface. 35 | func (f HandlerFunc) HandleMeasures(time time.Time, measures ...Measure) { 36 | f(time, measures...) 37 | } 38 | 39 | // MultiHandler constructs a handler which dispatches measures to all given 40 | // handlers. 41 | func MultiHandler(handlers ...Handler) Handler { 42 | multi := make([]Handler, 0, len(handlers)) 43 | 44 | for _, h := range handlers { 45 | if h != nil { 46 | if m, ok := h.(*multiHandler); ok { 47 | multi = append(multi, m.handlers...) // flatten multi handlers 48 | } else { 49 | multi = append(multi, h) 50 | } 51 | } 52 | } 53 | 54 | if len(multi) == 1 { 55 | return multi[0] 56 | } 57 | 58 | return &multiHandler{handlers: multi} 59 | } 60 | 61 | type multiHandler struct { 62 | handlers []Handler 63 | } 64 | 65 | func (m *multiHandler) HandleMeasures(time time.Time, measures ...Measure) { 66 | for _, h := range m.handlers { 67 | h.HandleMeasures(time, measures...) 68 | } 69 | } 70 | 71 | func (m *multiHandler) Flush() { 72 | for _, h := range m.handlers { 73 | flush(h) 74 | } 75 | } 76 | 77 | // FilteredHandler constructs a Handler that processes Measures with `filter` before forwarding to `h`. 78 | func FilteredHandler(h Handler, filter func([]Measure) []Measure) Handler { 79 | return &filteredHandler{handler: h, filter: filter} 80 | } 81 | 82 | type filteredHandler struct { 83 | handler Handler 84 | filter func([]Measure) []Measure 85 | } 86 | 87 | func (h *filteredHandler) HandleMeasures(time time.Time, measures ...Measure) { 88 | h.handler.HandleMeasures(time, h.filter(measures)...) 89 | } 90 | 91 | func (h *filteredHandler) Flush() { 92 | flush(h.handler) 93 | } 94 | 95 | // Discard is a handler that doesn't do anything with the measures it receives. 96 | var Discard = &discard{} 97 | 98 | type discard struct{} 99 | 100 | func (*discard) HandleMeasures(time.Time, ...Measure) {} 101 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package stats_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | "github.com/segmentio/stats/v5/statstest" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestMultiHandler(t *testing.T) { 14 | t.Run("calling HandleMeasures on a multi-handler dispatches to each handler", func(t *testing.T) { 15 | n := 0 16 | f := stats.HandlerFunc(func(time.Time, ...stats.Measure) { n++ }) 17 | m := stats.MultiHandler(f, f, f) 18 | 19 | m.HandleMeasures(time.Now()) 20 | 21 | if n != 3 { 22 | t.Error("bad number of calls to HandleMeasures:", n) 23 | } 24 | }) 25 | 26 | t.Run("calling Flush on a multi-handler flushes each handler", func(t *testing.T) { 27 | h1 := &statstest.Handler{} 28 | h2 := &statstest.Handler{} 29 | 30 | m := stats.MultiHandler(h1, h2) 31 | flush(m) 32 | flush(m) 33 | 34 | n1 := h1.FlushCalls() 35 | n2 := h2.FlushCalls() 36 | 37 | if n1 != 2 { 38 | t.Error("bad number of calls to Flush:", n1) 39 | } 40 | 41 | if n2 != 2 { 42 | t.Error("bad number of calls to Flush:", n2) 43 | } 44 | }) 45 | } 46 | 47 | func flush(h stats.Handler) { 48 | if f, ok := h.(stats.Flusher); ok { 49 | f.Flush() 50 | } 51 | } 52 | 53 | func TestFilteredHandler(t *testing.T) { 54 | t.Run("calling HandleMeasures on a filteredHandler processes the measures with the filter", func(t *testing.T) { 55 | handler := &statstest.Handler{} 56 | filter := func(ms []stats.Measure) []stats.Measure { 57 | measures := make([]stats.Measure, 0, len(ms)) 58 | for _, m := range ms { 59 | fields := make([]stats.Field, 0, len(m.Fields)) 60 | for _, f := range m.Fields { 61 | if f.Name == "a" { 62 | fields = append(fields, f) 63 | } 64 | } 65 | if len(fields) > 0 { 66 | measures = append(measures, stats.Measure{Name: m.Name, Fields: fields, Tags: m.Tags}) 67 | } 68 | } 69 | return measures 70 | } 71 | fh := stats.FilteredHandler(handler, filter) 72 | stats.Register(fh) 73 | 74 | stats.Observe("b", 1.23) 75 | assert.Equal(t, []stats.Measure{}, handler.Measures()) 76 | 77 | stats.Observe("a", 1.23) 78 | assert.Equal(t, []stats.Measure{ 79 | { 80 | Name: "stats.test", 81 | Fields: []stats.Field{stats.MakeField("a", 1.23, stats.Histogram)}, 82 | Tags: nil, 83 | }, 84 | }, handler.Measures()) 85 | 86 | stats.Incr("b") 87 | assert.Equal(t, []stats.Measure{ 88 | { 89 | Name: "stats.test", 90 | Fields: []stats.Field{stats.MakeField("a", 1.23, stats.Histogram)}, 91 | Tags: nil, 92 | }, 93 | }, handler.Measures()) 94 | 95 | stats.Incr("a") 96 | assert.Equal(t, []stats.Measure{ 97 | { 98 | Name: "stats.test", 99 | Fields: []stats.Field{stats.MakeField("a", 1.23, stats.Histogram)}, 100 | Tags: nil, 101 | }, 102 | { 103 | Name: "stats.test", 104 | Fields: []stats.Field{stats.MakeField("a", 1, stats.Counter)}, 105 | Tags: nil, 106 | }, 107 | }, handler.Measures()) 108 | }) 109 | 110 | t.Run("calling Flush on a FilteredHandler flushes the underlying handler", func(t *testing.T) { 111 | h := &statstest.Handler{} 112 | 113 | m := stats.FilteredHandler(h, func(ms []stats.Measure) []stats.Measure { return ms }) 114 | flush(m) 115 | 116 | assert.EqualValues(t, 1, h.FlushCalls(), "Flush should be called once") 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /httpstats/context.go: -------------------------------------------------------------------------------- 1 | package httpstats 2 | 3 | import ( 4 | "net/http" 5 | 6 | stats "github.com/segmentio/stats/v5" 7 | ) 8 | 9 | // RequestWithTags returns a shallow copy of req with its context updated to 10 | // include the given tags. If the context already contains tags, then those 11 | // are appended to, otherwise a new context containing the tags is created and 12 | // attached to the new request 13 | // ⚠️ : Using this may blow up the cardinality of your httpstats metrics. Use 14 | // with care for tags with low cardinalities. 15 | func RequestWithTags(req *http.Request, tags ...stats.Tag) *http.Request { 16 | if stats.ContextAddTags(req.Context(), tags...) { 17 | return req.WithContext(req.Context()) 18 | } 19 | return req.WithContext(stats.ContextWithTags(req.Context(), tags...)) 20 | } 21 | 22 | // RequestTags returns the tags associated with the request, if any. It 23 | // returns nil otherwise. 24 | func RequestTags(req *http.Request) []stats.Tag { 25 | return stats.ContextTags(req.Context()) 26 | } 27 | -------------------------------------------------------------------------------- /httpstats/context_test.go: -------------------------------------------------------------------------------- 1 | package httpstats 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | stats "github.com/segmentio/stats/v5" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // TestRequestContextTagPropagation verifies that the root ancestor tags are 15 | // updated in the event the context or request has children. It's nearly 16 | // identical to the context_test in the stats package itself, but we want to 17 | // keep this to ensure that changes to the request context code doesn't drift 18 | // and cause bugs. 19 | func TestRequestContextTagPropagation(t *testing.T) { 20 | // dummy request 21 | x := httptest.NewRequest(http.MethodGet, "http://example.com/blah", nil) 22 | 23 | y := RequestWithTags(x) 24 | assert.Equal(t, 0, len(RequestTags(y)), "Initialize request tags context value") 25 | RequestWithTags(y, stats.T("asdf", "qwer")) 26 | assert.Equal(t, 1, len(RequestTags(y)), "Adding tags should result new tags") 27 | assert.Equal(t, 0, len(RequestTags(x)), "Original request should have no tags (because no context with key)") 28 | 29 | // create a child request which creates a child context 30 | type contextVal struct{} 31 | z := y.WithContext(context.WithValue(y.Context(), contextVal{}, "important")) 32 | assert.Equal(t, 1, len(RequestTags(z)), "We should still be able to see original tags") 33 | 34 | // Add tags to the child context's reference to the original tag slice 35 | RequestWithTags(z, stats.T("zxcv", "uiop")) 36 | assert.Equal(t, 2, len(RequestTags(z)), "Updating tags should update local reference") 37 | assert.Equal(t, 2, len(RequestTags(y)), "Updating tags should update parent reference") 38 | assert.Equal(t, 0, len(RequestTags(x)), "Updating tags should not appear on original request") 39 | 40 | RequestWithTags(z, stats.T("a", "k"), stats.T("b", "k"), stats.T("c", "k"), stats.T("d", "k"), stats.T("e", "k"), stats.T("f", "k")) 41 | assert.Equal(t, 8, len(RequestTags(z)), "Updating tags should update local reference") 42 | assert.Equal(t, 8, len(RequestTags(y)), "Updating tags should update parent reference") 43 | assert.Equal(t, 0, len(RequestTags(x)), "Updating tags should not appear on original request") 44 | } 45 | -------------------------------------------------------------------------------- /httpstats/handler.go: -------------------------------------------------------------------------------- 1 | package httpstats 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | stats "github.com/segmentio/stats/v5" 10 | ) 11 | 12 | // NewHandler wraps h to produce metrics on the default engine for every request 13 | // received and every response sent. 14 | func NewHandler(h http.Handler) http.Handler { 15 | return NewHandlerWith(stats.DefaultEngine, h) 16 | } 17 | 18 | // NewHandlerWith wraps h to produce metrics on eng for every request received 19 | // and every response sent. 20 | func NewHandlerWith(eng *stats.Engine, h http.Handler) http.Handler { 21 | return &handler{ 22 | handler: h, 23 | eng: eng, 24 | } 25 | } 26 | 27 | type handler struct { 28 | handler http.Handler 29 | eng *stats.Engine 30 | } 31 | 32 | func (h *handler) ServeHTTP(res http.ResponseWriter, req *http.Request) { 33 | m := &metrics{} 34 | 35 | req = RequestWithTags(req) 36 | w := &responseWriter{ 37 | ResponseWriter: res, 38 | eng: h.eng, 39 | req: req, 40 | metrics: m, 41 | start: time.Now(), 42 | } 43 | defer w.complete() 44 | 45 | b := &requestBody{ 46 | body: req.Body, 47 | eng: h.eng, 48 | req: req, 49 | metrics: m, 50 | op: "read", 51 | } 52 | defer b.close() 53 | 54 | req.Body = b 55 | h.handler.ServeHTTP(w, req) 56 | } 57 | 58 | type responseWriter struct { 59 | http.ResponseWriter 60 | start time.Time 61 | eng *stats.Engine 62 | req *http.Request 63 | metrics *metrics 64 | status int 65 | bytes int 66 | wroteHeader bool 67 | wroteStats bool 68 | } 69 | 70 | func (w *responseWriter) WriteHeader(status int) { 71 | if !w.wroteHeader { 72 | w.wroteHeader = true 73 | w.status = status 74 | w.ResponseWriter.WriteHeader(status) 75 | } 76 | } 77 | 78 | func (w *responseWriter) Write(b []byte) (n int, err error) { 79 | if !w.wroteHeader { 80 | w.wroteHeader = true 81 | w.status = http.StatusOK 82 | } 83 | 84 | if n, err = w.ResponseWriter.Write(b); n > 0 { 85 | w.bytes += n 86 | } 87 | 88 | return 89 | } 90 | 91 | func (w *responseWriter) Hijack() (conn net.Conn, buf *bufio.ReadWriter, err error) { 92 | if conn, buf, err = w.ResponseWriter.(http.Hijacker).Hijack(); err == nil { 93 | w.wroteHeader = true 94 | w.complete() 95 | } 96 | return 97 | } 98 | 99 | func (w *responseWriter) complete() { 100 | if w.wroteStats { 101 | return 102 | } 103 | w.wroteStats = true 104 | 105 | if !w.wroteHeader { 106 | w.wroteHeader = true 107 | w.status = http.StatusOK 108 | } 109 | 110 | now := time.Now() 111 | res := &http.Response{ 112 | ProtoMajor: w.req.ProtoMajor, 113 | ProtoMinor: w.req.ProtoMinor, 114 | Proto: w.req.Proto, 115 | StatusCode: w.status, 116 | Header: w.Header(), 117 | Request: w.req, 118 | ContentLength: -1, 119 | } 120 | 121 | w.metrics.observeResponse(res, "write", w.bytes, now.Sub(w.start)) 122 | w.eng.ReportAt(w.start, w.metrics, RequestTags(w.req)...) 123 | } 124 | -------------------------------------------------------------------------------- /httpstats/handler_test.go: -------------------------------------------------------------------------------- 1 | package httpstats 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | stats "github.com/segmentio/stats/v5" 11 | "github.com/segmentio/stats/v5/statstest" 12 | ) 13 | 14 | func TestHandler(t *testing.T) { 15 | h := &statstest.Handler{} 16 | e := stats.NewEngine("", h) 17 | 18 | server := httptest.NewServer(NewHandlerWith(e, http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 19 | io.ReadAll(req.Body) 20 | _ = RequestWithTags(req, stats.T("foo", "bar")) 21 | res.WriteHeader(http.StatusOK) 22 | res.Write([]byte("Hello World")) 23 | }))) 24 | defer server.Close() 25 | 26 | res, err := http.Post(server.URL, "text/plain", strings.NewReader("Hi")) 27 | if err != nil { 28 | t.Error(err) 29 | return 30 | } 31 | io.ReadAll(res.Body) 32 | res.Body.Close() 33 | 34 | e.Flush() 35 | measures := h.Measures() 36 | 37 | if len(measures) == 0 { 38 | t.Error("no measures reported by http handler") 39 | } 40 | 41 | tagSeen := false 42 | for _, m := range measures { 43 | for _, tag := range m.Tags { 44 | if tag.Name == "bucket" { 45 | switch tag.Value { 46 | case "2xx", "": 47 | default: 48 | t.Errorf("invalid bucket in measure event tags: %#v\n%#v", tag, m) 49 | } 50 | } 51 | if tag.Name == "foo" { 52 | tagSeen = true 53 | if tag.Value != "bar" { 54 | t.Errorf("user-added tag didn't match expected. tag: %#v\n%#v", tag, m) 55 | } 56 | } 57 | } 58 | } 59 | if !tagSeen { 60 | t.Errorf("did not see user-added tag for wrapped request. measures: %#v", measures) 61 | } 62 | 63 | for _, m := range measures { 64 | t.Log(m) 65 | } 66 | } 67 | 68 | func TestHandlerHijack(t *testing.T) { 69 | h := &statstest.Handler{} 70 | e := stats.NewEngine("", h) 71 | 72 | server := httptest.NewServer(NewHandlerWith(e, http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) { 73 | // make sure the response writer supports hijacking 74 | conn, _, _ := res.(http.Hijacker).Hijack() 75 | conn.Close() 76 | }))) 77 | defer server.Close() 78 | 79 | if _, err := http.Post(server.URL, "text/plain", strings.NewReader("Hi")); err == nil { 80 | t.Error("no error was reported by the http client") 81 | } 82 | 83 | measures := h.Measures() 84 | 85 | if len(measures) == 0 { 86 | t.Error("no measures reported by hijacked http handler") 87 | } 88 | 89 | for _, m := range measures { 90 | t.Log(m) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /httpstats/transport.go: -------------------------------------------------------------------------------- 1 | package httpstats 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | ) 9 | 10 | // NewTransport wraps t to produce metrics on the default engine for every request 11 | // sent and every response received. 12 | func NewTransport(t http.RoundTripper) http.RoundTripper { 13 | return NewTransportWith(stats.DefaultEngine, t) 14 | } 15 | 16 | // NewTransportWith wraps t to produce metrics on eng for every request sent and 17 | // every response received. 18 | func NewTransportWith(eng *stats.Engine, t http.RoundTripper) http.RoundTripper { 19 | return &transport{ 20 | transport: t, 21 | eng: eng, 22 | } 23 | } 24 | 25 | type transport struct { 26 | transport http.RoundTripper 27 | eng *stats.Engine 28 | } 29 | 30 | // RoundTrip implements http.RoundTripper. 31 | func (t *transport) RoundTrip(req *http.Request) (res *http.Response, err error) { 32 | start := time.Now() 33 | rtrip := t.transport 34 | eng := t.eng 35 | 36 | if rtrip == nil { 37 | rtrip = http.DefaultTransport 38 | } 39 | 40 | if tags := RequestTags(req); len(tags) > 0 { 41 | eng = eng.WithTags(tags...) 42 | } 43 | 44 | if req.Body == nil { 45 | req.Body = &nullBody{} 46 | } 47 | 48 | m := &metrics{} 49 | 50 | req.Body = &requestBody{ 51 | eng: eng, 52 | req: req, 53 | metrics: m, 54 | body: req.Body, 55 | op: "write", 56 | } 57 | 58 | res, err = rtrip.RoundTrip(req) 59 | // safe guard, the transport should have done it already 60 | req.Body.Close() // nolint 61 | 62 | if err != nil { 63 | m.observeError(time.Since(start)) 64 | eng.ReportAt(start, m) 65 | return 66 | } 67 | 68 | res.Body = &responseBody{ 69 | eng: eng, 70 | res: res, 71 | metrics: m, 72 | body: res.Body, 73 | op: "read", 74 | start: start, 75 | } 76 | 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /httpstats/transport_test.go: -------------------------------------------------------------------------------- 1 | package httpstats 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | stats "github.com/segmentio/stats/v5" 11 | "github.com/segmentio/stats/v5/statstest" 12 | ) 13 | 14 | func TestTransport(t *testing.T) { 15 | newRequest := func(method, path string, body io.Reader) *http.Request { 16 | req, _ := http.NewRequest(method, path, body) 17 | return req 18 | } 19 | 20 | for _, transport := range []http.RoundTripper{ 21 | nil, 22 | &http.Transport{}, 23 | http.DefaultTransport, 24 | http.DefaultClient.Transport, 25 | } { 26 | t.Run("", func(t *testing.T) { 27 | for _, req := range []*http.Request{ 28 | newRequest("GET", "/", nil), 29 | newRequest("POST", "/", strings.NewReader("Hi")), 30 | } { 31 | t.Run("", func(t *testing.T) { 32 | h := &statstest.Handler{} 33 | e := stats.NewEngine("", h) 34 | 35 | server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 36 | io.ReadAll(req.Body) 37 | res.Write([]byte("Hello World!")) 38 | })) 39 | defer server.Close() 40 | 41 | httpc := &http.Client{ 42 | Transport: NewTransportWith(e, transport), 43 | } 44 | 45 | req.URL.Scheme = "http" 46 | req.URL.Host = server.URL[7:] 47 | 48 | res, err := httpc.Do(req) 49 | if err != nil { 50 | t.Error(err) 51 | return 52 | } 53 | io.ReadAll(res.Body) 54 | res.Body.Close() 55 | 56 | if len(h.Measures()) == 0 { 57 | t.Error("no measures reported by http handler") 58 | } 59 | 60 | for _, m := range h.Measures() { 61 | for _, tag := range m.Tags { 62 | if tag.Name == "bucket" { 63 | switch tag.Value { 64 | case "2xx", "": 65 | default: 66 | t.Errorf("invalid bucket in measure event tags: %#v\n%#v", tag, m) 67 | } 68 | } 69 | } 70 | } 71 | }) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func TestTransportError(t *testing.T) { 78 | h := &statstest.Handler{} 79 | e := stats.NewEngine("", h) 80 | 81 | server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) { 82 | conn, _, _ := res.(http.Hijacker).Hijack() 83 | conn.Close() 84 | })) 85 | defer server.Close() 86 | 87 | httpc := &http.Client{ 88 | Transport: NewTransportWith(e, &http.Transport{}), 89 | } 90 | 91 | if _, err := httpc.Post(server.URL, "text/plain", strings.NewReader("Hi")); err == nil { 92 | t.Error("no error was reported by the http client") 93 | } 94 | 95 | measures := h.Measures() 96 | 97 | if len(measures) == 0 { 98 | t.Error("no measures reported by hijacked http handler") 99 | } 100 | 101 | for _, m := range measures { 102 | t.Log(m) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /influxdb/client_test.go: -------------------------------------------------------------------------------- 1 | package influxdb 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | stats "github.com/segmentio/stats/v5" 12 | ) 13 | 14 | func DisabledTestClient(t *testing.T) { 15 | transport := &errorCaptureTransport{ 16 | RoundTripper: http.DefaultTransport, 17 | } 18 | 19 | client := NewClientWith(ClientConfig{ 20 | Address: DefaultAddress, 21 | Database: "test-db", 22 | Transport: transport, 23 | }) 24 | 25 | if err := client.CreateDB("test-db"); err != nil { 26 | t.Error(err) 27 | } 28 | 29 | for i := 0; i != 1000; i++ { 30 | client.HandleMeasures(time.Now(), stats.Measure{ 31 | Name: "request", 32 | Fields: []stats.Field{ 33 | {Name: "count", Value: stats.ValueOf(5)}, 34 | {Name: "rtt", Value: stats.ValueOf(100 * time.Millisecond)}, 35 | }, 36 | Tags: []stats.Tag{ 37 | stats.T("answer", "42"), 38 | stats.T("hello", "world"), 39 | }, 40 | }) 41 | } 42 | 43 | client.Close() 44 | 45 | if transport.err != nil { 46 | t.Error(transport.err) 47 | } 48 | } 49 | 50 | func BenchmarkClient(b *testing.B) { 51 | for _, N := range []int{1, 10, 100} { 52 | b.Run(fmt.Sprintf("write a batch of %d measures to a client", N), func(b *testing.B) { 53 | client := NewClientWith(ClientConfig{ 54 | Address: DefaultAddress, 55 | Transport: &discardTransport{}, 56 | }) 57 | 58 | timestamp := time.Now() 59 | measures := make([]stats.Measure, N) 60 | 61 | for i := range measures { 62 | measures[i] = stats.Measure{ 63 | Name: "benchmark.test.metric", 64 | Fields: []stats.Field{ 65 | {Name: "value", Value: stats.ValueOf(42)}, 66 | }, 67 | Tags: []stats.Tag{ 68 | stats.T("answer", "42"), 69 | stats.T("hello", "world"), 70 | }, 71 | } 72 | } 73 | 74 | b.RunParallel(func(pb *testing.PB) { 75 | for pb.Next() { 76 | client.HandleMeasures(timestamp, measures...) 77 | } 78 | }) 79 | }) 80 | } 81 | } 82 | 83 | type errorCaptureTransport struct { 84 | http.RoundTripper 85 | err error 86 | } 87 | 88 | func (t *errorCaptureTransport) RoundTrip(req *http.Request) (*http.Response, error) { 89 | res, err := t.RoundTripper.RoundTrip(req) 90 | 91 | if t.err == nil { 92 | if err != nil { 93 | t.err = err 94 | } else if res.StatusCode >= 300 { 95 | t.err = fmt.Errorf("%s %s: %d", req.Method, req.URL, res.StatusCode) 96 | } 97 | } 98 | 99 | return res, err 100 | } 101 | 102 | type discardTransport struct{} 103 | 104 | func (t *discardTransport) RoundTrip(req *http.Request) (*http.Response, error) { 105 | return &http.Response{ 106 | StatusCode: http.StatusOK, 107 | Body: io.NopCloser(strings.NewReader("")), 108 | Request: req, 109 | }, nil 110 | } 111 | -------------------------------------------------------------------------------- /influxdb/measure.go: -------------------------------------------------------------------------------- 1 | package influxdb 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | ) 9 | 10 | // AppendMeasure is a formatting routine to append the InflxDB line protocol 11 | // representation of a measure to a memory buffer. 12 | func AppendMeasure(b []byte, t time.Time, m stats.Measure) []byte { 13 | b = append(b, m.Name...) 14 | 15 | for _, tag := range m.Tags { 16 | b = append(b, ',') 17 | b = append(b, tag.Name...) 18 | b = append(b, '=') 19 | b = append(b, tag.Value...) 20 | } 21 | 22 | for i, field := range m.Fields { 23 | if len(field.Name) == 0 { 24 | field.Name = "value" 25 | } 26 | 27 | if i == 0 { 28 | b = append(b, ' ') 29 | } else { 30 | b = append(b, ',') 31 | } 32 | 33 | b = append(b, field.Name...) 34 | b = append(b, '=') 35 | 36 | switch v := field.Value; v.Type() { 37 | case stats.Null: 38 | case stats.Bool: 39 | if v.Bool() { 40 | b = append(b, "true"...) 41 | } else { 42 | b = append(b, "false"...) 43 | } 44 | case stats.Int: 45 | b = strconv.AppendInt(b, v.Int(), 10) 46 | case stats.Uint: 47 | b = strconv.AppendUint(b, v.Uint(), 10) 48 | case stats.Float: 49 | b = strconv.AppendFloat(b, v.Float(), 'g', -1, 64) 50 | case stats.Duration: 51 | b = strconv.AppendFloat(b, v.Duration().Seconds(), 'g', -1, 64) 52 | } 53 | } 54 | 55 | b = append(b, ' ') 56 | b = strconv.AppendInt(b, t.UnixNano(), 10) 57 | 58 | return append(b, '\n') 59 | } 60 | -------------------------------------------------------------------------------- /influxdb/measure_test.go: -------------------------------------------------------------------------------- 1 | package influxdb 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | ) 9 | 10 | var ( 11 | timestamp = time.Date(2017, 7, 23, 3, 36, 0, 123456789, time.UTC) 12 | testMetrics = []struct { 13 | m stats.Measure 14 | s string 15 | }{ 16 | { 17 | m: stats.Measure{ 18 | Name: "request", 19 | Fields: []stats.Field{ 20 | {Name: "count", Value: stats.ValueOf(5)}, 21 | }, 22 | }, 23 | s: `request count=5 1500780960123456789`, 24 | }, 25 | 26 | { 27 | m: stats.Measure{ 28 | Name: "request", 29 | Fields: []stats.Field{ 30 | {Name: "count", Value: stats.ValueOf(5)}, 31 | {Name: "rtt", Value: stats.ValueOf(100 * time.Millisecond)}, 32 | }, 33 | Tags: []stats.Tag{ 34 | stats.T("answer", "42"), 35 | stats.T("hello", "world"), 36 | }, 37 | }, 38 | s: `request,answer=42,hello=world count=5,rtt=0.1 1500780960123456789`, 39 | }, 40 | } 41 | ) 42 | 43 | func TestAppendMetric(t *testing.T) { 44 | for _, test := range testMetrics { 45 | t.Run(test.s, func(t *testing.T) { 46 | if s := string(AppendMeasure(nil, timestamp, test.m)); s != (test.s + "\n") { 47 | t.Error("bad metric representation:") 48 | t.Log("expected:", test.s) 49 | t.Log("found: ", s) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /iostats/io.go: -------------------------------------------------------------------------------- 1 | package iostats 2 | 3 | import "io" 4 | 5 | // CountReader is an io.Reader that counts how many bytes are read by calls to 6 | // the Read method. 7 | type CountReader struct { 8 | R io.Reader 9 | N int 10 | } 11 | 12 | // Read satisfies the io.Reader interface. 13 | func (r *CountReader) Read(b []byte) (n int, err error) { 14 | if n, err = r.R.Read(b); n > 0 { 15 | r.N += n 16 | } 17 | return 18 | } 19 | 20 | // CountWriter is an io.Writer that counts how many bytes are written by calls 21 | // to the Write method. 22 | type CountWriter struct { 23 | W io.Writer 24 | N int 25 | } 26 | 27 | // Write satisfies the io.Writer interface. 28 | func (w *CountWriter) Write(b []byte) (n int, err error) { 29 | if n, err = w.W.Write(b); n > 0 { 30 | w.N += n 31 | } 32 | return 33 | } 34 | 35 | // ReaderFunc makes it possible for function types to be used as io.Reader. 36 | type ReaderFunc func([]byte) (int, error) 37 | 38 | // Read calls f(b). 39 | func (f ReaderFunc) Read(b []byte) (int, error) { return f(b) } 40 | 41 | // WriterFunc makes it possible for function types to be used as io.Writer. 42 | type WriterFunc func([]byte) (int, error) 43 | 44 | // Write calls f(b). 45 | func (f WriterFunc) Write(b []byte) (int, error) { return f(b) } 46 | 47 | // CloserFunc makes it possible for function types to be used as io.Closer. 48 | type CloserFunc func() error 49 | 50 | // Close calls f. 51 | func (f CloserFunc) Close() error { return f() } 52 | -------------------------------------------------------------------------------- /iostats/io_test.go: -------------------------------------------------------------------------------- 1 | package iostats 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestCountReader(t *testing.T) { 11 | tests := []struct { 12 | s string 13 | }{ 14 | { 15 | s: "", 16 | }, 17 | { 18 | s: "Hello World!", 19 | }, 20 | } 21 | 22 | for _, test := range tests { 23 | c := &CountReader{R: strings.NewReader(test.s)} 24 | b := make([]byte, len(test.s)) 25 | 26 | if n, err := c.Read(b); err != nil && err != io.EOF { 27 | t.Error(err) 28 | } else if n != len(test.s) { 29 | t.Errorf("invalid byte count returned by the reader: %d != %d", len(test.s), n) 30 | } else if s := string(b); s != test.s { 31 | t.Errorf("invalid content returned by the reader: %#v != %#v", test.s, s) 32 | } 33 | } 34 | } 35 | 36 | func TestCountWriter(t *testing.T) { 37 | tests := []struct { 38 | s string 39 | }{ 40 | { 41 | s: "", 42 | }, 43 | { 44 | s: "Hello World!", 45 | }, 46 | } 47 | 48 | for _, test := range tests { 49 | b := &bytes.Buffer{} 50 | c := &CountWriter{W: b} 51 | 52 | if n, err := c.Write([]byte(test.s)); err != nil { 53 | t.Error(err) 54 | } else if n != len(test.s) { 55 | t.Errorf("invalid byte count returned by the writer: %d != %d", len(test.s), n) 56 | } else if s := b.String(); s != test.s { 57 | t.Errorf("invalid content returned by the writer: %#v != %#v", test.s, s) 58 | } 59 | } 60 | } 61 | 62 | func TestReaderFunc(t *testing.T) { 63 | r := ReaderFunc(strings.NewReader("Hello World!").Read) 64 | b := make([]byte, 20) 65 | 66 | if n, err := r.Read(b); err != nil { 67 | t.Errorf("ReaderFunc.Read was expected to return no error but got %v", err) 68 | } else if n != 12 { 69 | t.Errorf("ReaderFunc.Read was expected to return 12 bytes but got %d", n) 70 | } else if s := string(b[:n]); s != "Hello World!" { 71 | t.Errorf("ReaderFunc.Read filled the buffer with invalid content: %#v", s) 72 | } 73 | } 74 | 75 | func TestWriterFunc(t *testing.T) { 76 | b := &bytes.Buffer{} 77 | w := WriterFunc(b.Write) 78 | 79 | if n, err := w.Write([]byte("Hello World!")); err != nil { 80 | t.Errorf("WriterFunc.Write was expected to return no error but got %v", err) 81 | } else if n != 12 { 82 | t.Errorf("WriterFunc.Write was expected to return 12 bytes but got %d", n) 83 | } else if s := b.String(); s != "Hello World!" { 84 | t.Errorf("WriterFunc.Write filled the buffer with invalid content: %#v", s) 85 | } 86 | } 87 | 88 | func TestCloserFunc(t *testing.T) { 89 | c := CloserFunc(func() error { return io.EOF }) 90 | 91 | if err := c.Close(); err != io.EOF { 92 | t.Errorf("CloseFunc.Close returned an invalid error, expected EOF but got %v", err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /netstats/handler.go: -------------------------------------------------------------------------------- 1 | package netstats 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | ) 9 | 10 | // Handler is an interface that can be implemented by types that serve network 11 | // connections. 12 | type Handler interface { 13 | ServeConn(ctx context.Context, conn net.Conn) 14 | } 15 | 16 | // NewHandler returns a Handler object that warps hdl and produces 17 | // metrics on the default engine. 18 | func NewHandler(hdl Handler) Handler { 19 | return NewHandlerWith(stats.DefaultEngine, hdl) 20 | } 21 | 22 | // NewHandlerWith returns a Handler object that warps hdl and produces 23 | // metrics on eng. 24 | func NewHandlerWith(eng *stats.Engine, hdl Handler) Handler { 25 | return &handler{ 26 | handler: hdl, 27 | eng: eng, 28 | } 29 | } 30 | 31 | type handler struct { 32 | handler Handler 33 | eng *stats.Engine 34 | } 35 | 36 | func (h *handler) ServeConn(ctx context.Context, conn net.Conn) { 37 | h.handler.ServeConn(ctx, NewConnWith(h.eng, conn)) 38 | } 39 | -------------------------------------------------------------------------------- /netstats/handler_test.go: -------------------------------------------------------------------------------- 1 | package netstats 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | ) 8 | 9 | type testHandler struct { 10 | ok bool 11 | } 12 | 13 | func (h *testHandler) ServeConn(context.Context, net.Conn) { 14 | h.ok = true 15 | } 16 | 17 | func TestHandler(t *testing.T) { 18 | conn := &testConn{} 19 | test := &testHandler{} 20 | 21 | handler := NewHandler(test) 22 | handler.ServeConn(context.Background(), conn) 23 | 24 | if !test.ok { 25 | t.Error("the connection handler wasn't called") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /netstats/listener.go: -------------------------------------------------------------------------------- 1 | package netstats 2 | 3 | import ( 4 | "net" 5 | "sync/atomic" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | ) 9 | 10 | // NewListener returns a new net.Listener which uses the stats.DefaultEngine. 11 | func NewListener(lstn net.Listener) net.Listener { 12 | return NewListenerWith(stats.DefaultEngine, lstn) 13 | } 14 | 15 | // NewListenerWith returns a new net.Listener with the provided *stats.Engine. 16 | func NewListenerWith(eng *stats.Engine, lstn net.Listener) net.Listener { 17 | return &listener{ 18 | lstn: lstn, 19 | eng: eng, 20 | } 21 | } 22 | 23 | type listener struct { 24 | lstn net.Listener 25 | eng *stats.Engine 26 | closed uint32 27 | } 28 | 29 | func (l *listener) Accept() (conn net.Conn, err error) { 30 | if conn, err = l.lstn.Accept(); err != nil { 31 | if atomic.LoadUint32(&l.closed) == 0 { 32 | l.error("accept", err) 33 | } 34 | } 35 | 36 | if conn != nil { 37 | conn = NewConnWith(l.eng, conn) 38 | } 39 | 40 | return 41 | } 42 | 43 | func (l *listener) Close() (err error) { 44 | atomic.StoreUint32(&l.closed, 1) 45 | return l.lstn.Close() 46 | } 47 | 48 | func (l *listener) Addr() net.Addr { 49 | return l.lstn.Addr() 50 | } 51 | 52 | func (l *listener) error(op string, err error) { 53 | if !isTemporary(err) { 54 | l.eng.Incr("conn.error.count", 55 | stats.T("operation", op), 56 | stats.T("protocol", l.Addr().Network()), 57 | ) 58 | } 59 | } 60 | 61 | func isTemporary(err error) bool { 62 | e, ok := err.(interface { 63 | Temporary() bool 64 | }) 65 | return ok && e.Temporary() 66 | } 67 | -------------------------------------------------------------------------------- /netstats/listener_test.go: -------------------------------------------------------------------------------- 1 | package netstats 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "testing" 7 | 8 | stats "github.com/segmentio/stats/v5" 9 | "github.com/segmentio/stats/v5/statstest" 10 | "github.com/segmentio/stats/v5/version" 11 | ) 12 | 13 | func TestListener(t *testing.T) { 14 | initValue := stats.GoVersionReportingEnabled 15 | stats.GoVersionReportingEnabled = false 16 | defer func() { stats.GoVersionReportingEnabled = initValue }() 17 | h := &statstest.Handler{} 18 | e := stats.NewEngine("netstats.test", h) 19 | 20 | lstn := NewListenerWith(e, testLstn{}) 21 | 22 | conn, err := lstn.Accept() 23 | if err != nil { 24 | t.Error(err) 25 | return 26 | } 27 | 28 | conn.Close() 29 | lstn.Close() 30 | 31 | expected := []stats.Measure{ 32 | { 33 | Name: "netstats.test.conn.open", 34 | Fields: []stats.Field{stats.MakeField("count", 1, stats.Counter)}, 35 | Tags: []stats.Tag{stats.T("protocol", "tcp")}, 36 | }, 37 | { 38 | Name: "netstats.test.conn.close", 39 | Fields: []stats.Field{stats.MakeField("count", 1, stats.Counter)}, 40 | Tags: []stats.Tag{stats.T("protocol", "tcp")}, 41 | }, 42 | } 43 | 44 | if !reflect.DeepEqual(expected, h.Measures()) { 45 | t.Error("bad measures:") 46 | t.Logf("expected: %v", expected) 47 | t.Logf("found: %v", h.Measures()) 48 | } 49 | } 50 | 51 | func TestListenerError(t *testing.T) { 52 | h := &statstest.Handler{} 53 | e := stats.NewEngine("netstats.test", h) 54 | 55 | lstn := NewListenerWith(e, testLstn{err: errTest}) 56 | 57 | _, err := lstn.Accept() 58 | if err != errTest { 59 | t.Error(err) 60 | return 61 | } 62 | 63 | lstn.Close() 64 | 65 | measures := h.Measures() 66 | t.Run("CheckGoVersionEmitted", func(t *testing.T) { 67 | if version.DevelGoVersion() { 68 | t.Skip("No metrics emitted if compiled with Go devel version") 69 | } 70 | measurePassed := false 71 | for _, measure := range measures { 72 | if measure.Name != "go_version" { 73 | continue 74 | } 75 | for _, tag := range measure.Tags { 76 | if tag.Name != "go_version" { 77 | continue 78 | } 79 | if tag.Value == version.GoVersion() { 80 | measurePassed = true 81 | } 82 | } 83 | } 84 | if !measurePassed { 85 | t.Errorf("did not find correct 'go_version' tag for measure: %#v\n", measures) 86 | } 87 | }) 88 | var foundMetric stats.Measure 89 | for i := range measures { 90 | if measures[i].Name == "netstats.test.conn.error" { 91 | foundMetric = measures[i] 92 | break 93 | } 94 | } 95 | if foundMetric.Name == "" { 96 | t.Errorf("did not find netstats metric: %v", measures) 97 | } 98 | 99 | expected := stats.Measure{ 100 | Name: "netstats.test.conn.error", 101 | Fields: []stats.Field{stats.MakeField("count", 1, stats.Counter)}, 102 | Tags: []stats.Tag{stats.T("operation", "accept"), stats.T("protocol", "tcp")}, 103 | } 104 | if !reflect.DeepEqual(expected, foundMetric) { 105 | t.Error("bad measures:") 106 | t.Logf("expected: %v", expected) 107 | t.Logf("found: %v", h.Measures()) 108 | } 109 | } 110 | 111 | type testLstn struct { 112 | conn testConn 113 | err error 114 | } 115 | 116 | func (lstn testLstn) Accept() (net.Conn, error) { 117 | if lstn.err != nil { 118 | return nil, lstn.err 119 | } 120 | return &lstn.conn, nil 121 | } 122 | 123 | func (lstn testLstn) Close() error { 124 | return lstn.err 125 | } 126 | 127 | func (lstn testLstn) Addr() net.Addr { 128 | return testLocalAddr 129 | } 130 | -------------------------------------------------------------------------------- /otlp/client.go: -------------------------------------------------------------------------------- 1 | package otlp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | 11 | colmetricpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | type Client interface { 16 | Handle(context.Context, *colmetricpb.ExportMetricsServiceRequest) error 17 | } 18 | 19 | // HTTPClient implements the Client interface and is used to export metrics to 20 | // an OpenTelemetry Collector through the HTTP interface. 21 | // 22 | // The current implementation is a fire and forget approach where we do not retry 23 | // or buffer any failed-to-flush data on the client. 24 | type HTTPClient struct { 25 | client *http.Client 26 | endpoint string 27 | } 28 | 29 | func NewHTTPClient(endpoint string) *HTTPClient { 30 | return &HTTPClient{ 31 | // TODO: add sane default timeout configuration. 32 | client: http.DefaultClient, 33 | endpoint: endpoint, 34 | } 35 | } 36 | 37 | func (c *HTTPClient) Handle(ctx context.Context, request *colmetricpb.ExportMetricsServiceRequest) error { 38 | rawReq, err := proto.Marshal(request) 39 | if err != nil { 40 | return fmt.Errorf("failed to marshal request: %s", err) 41 | } 42 | 43 | httpReq, err := newRequest(ctx, c.endpoint, rawReq) 44 | if err != nil { 45 | return fmt.Errorf("failed to create HTTP request: %s", err) 46 | } 47 | 48 | return c.do(httpReq) 49 | } 50 | 51 | // TODO: deal with requests failures and retries. We potentially want to implement 52 | // 53 | // some kind of retry mechanism with expotential backoff + short time window. 54 | func (c *HTTPClient) do(req *http.Request) error { 55 | resp, err := c.client.Do(req) 56 | if err != nil { 57 | return err 58 | } 59 | defer resp.Body.Close() 60 | 61 | msg, err := io.ReadAll(resp.Body) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if resp.StatusCode != http.StatusOK { 67 | return fmt.Errorf("failed to send data to collector, code: %d, error: %s", 68 | resp.StatusCode, 69 | string(msg), 70 | ) 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func newRequest(ctx context.Context, endpoint string, data []byte) (*http.Request, error) { 77 | u, err := url.Parse(endpoint) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), nil) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | req.Header.Set("Content-Type", "application/x-protobuf") 88 | req.Header.Set("User-Agent", "segmentio/stats") 89 | 90 | req.Body = io.NopCloser(bytes.NewReader(data)) 91 | return req, nil 92 | } 93 | -------------------------------------------------------------------------------- /otlp/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/segmentio/stats/v5/otlp 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/segmentio/stats/v5 v5.0.1 7 | go.opentelemetry.io/proto/otlp v1.3.1 8 | google.golang.org/protobuf v1.34.2 9 | ) 10 | 11 | require ( 12 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 13 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect 14 | golang.org/x/net v0.38.0 // indirect 15 | golang.org/x/sys v0.31.0 // indirect 16 | golang.org/x/text v0.23.0 // indirect 17 | google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect 18 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect 19 | google.golang.org/grpc v1.64.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /otlp/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 3 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= 4 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= 7 | github.com/segmentio/stats/v5/util/objconv v1.0.1 h1:QjfLzwriJj40JibCV3MGSEiAoXixbp4ybhwfTB8RXOM= 8 | github.com/segmentio/stats/v5 v5.0.1 h1:oTufwnhBP6Yr7z9aWHBRAh3w/+opAIl/+1qj+U90pR0= 9 | github.com/segmentio/stats/v5 v5.0.1/go.mod h1:Dn+b5nF2pDFdDykw/bEu+Q1u0WCWebDqcyo4pPNuJ+w= 10 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 11 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 12 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 13 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= 14 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= 15 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 16 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 17 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 18 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 19 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 20 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 21 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 22 | google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No= 23 | google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= 24 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA= 25 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= 26 | google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= 27 | google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= 28 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 29 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | -------------------------------------------------------------------------------- /otlp/metric.go: -------------------------------------------------------------------------------- 1 | package otlp 2 | 3 | import ( 4 | "hash/maphash" 5 | "sort" 6 | "time" 7 | 8 | "github.com/segmentio/stats/v5" 9 | ) 10 | 11 | type metric struct { 12 | measureName string 13 | fieldName string 14 | fieldType stats.FieldType 15 | flushed bool 16 | time time.Time 17 | value stats.Value 18 | sum float64 19 | sign uint64 20 | count uint64 21 | buckets metricBuckets 22 | tags []stats.Tag 23 | } 24 | 25 | func (m *metric) signature() uint64 { 26 | h := maphash.Hash{} 27 | h.SetSeed(hashseed) 28 | h.WriteString(m.measureName) 29 | h.WriteString(m.fieldName) 30 | 31 | sort.Slice(m.tags, func(i, j int) bool { 32 | return m.tags[i].Name > m.tags[j].Name 33 | }) 34 | 35 | for _, tag := range m.tags { 36 | h.WriteString(tag.String()) 37 | } 38 | 39 | return h.Sum64() 40 | } 41 | 42 | func (m *metric) add(v stats.Value) stats.Value { 43 | switch v.Type() { 44 | case stats.Int: 45 | return stats.ValueOf(m.value.Int() + v.Int()) 46 | case stats.Uint: 47 | return stats.ValueOf(m.value.Uint() + v.Uint()) 48 | case stats.Float: 49 | return stats.ValueOf(m.value.Float() + v.Float()) 50 | } 51 | return v 52 | } 53 | 54 | type bucket struct { 55 | count uint64 56 | upperBound float64 57 | } 58 | 59 | type metricBuckets []bucket 60 | 61 | func makeMetricBuckets(buckets []stats.Value) metricBuckets { 62 | b := make(metricBuckets, len(buckets)) 63 | for i := range buckets { 64 | b[i].upperBound = valueOf(buckets[i]) 65 | } 66 | return b 67 | } 68 | 69 | func (b metricBuckets) update(v float64) { 70 | for i := range b { 71 | if v <= b[i].upperBound { 72 | b[i].count++ 73 | break 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /procstats/collector.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | import ( 4 | "io" 5 | "runtime" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // Collector is an interface that wraps the Collect() method. 11 | type Collector interface { 12 | Collect() 13 | } 14 | 15 | // CollectorFunc is a type alias for func(). 16 | type CollectorFunc func() 17 | 18 | // Collect calls the underling CollectorFunc func(). 19 | func (f CollectorFunc) Collect() { f() } 20 | 21 | // Config contains a Collector and a time.Duration called CollectInterval. 22 | type Config struct { 23 | Collector Collector 24 | CollectInterval time.Duration 25 | } 26 | 27 | // MultiCollector coalesces a variadic number of Collectors 28 | // and returns a single Collector. 29 | func MultiCollector(collectors ...Collector) Collector { 30 | return CollectorFunc(func() { 31 | for _, c := range collectors { 32 | c.Collect() 33 | } 34 | }) 35 | } 36 | 37 | // StartCollector starts a Collector with a default Config. 38 | func StartCollector(collector Collector) io.Closer { 39 | return StartCollectorWith(Config{Collector: collector}) 40 | } 41 | 42 | // StartCollectorWith starts a Collector with the provided Config. 43 | func StartCollectorWith(config Config) io.Closer { 44 | config = setConfigDefaults(config) 45 | 46 | stop := make(chan struct{}) 47 | join := make(chan struct{}) 48 | 49 | go func() { 50 | // Locks the OS thread, stats collection heavily relies on blocking 51 | // syscalls, letting other goroutines execute on the same thread 52 | // increases the chance for the Go runtime to detected that the thread 53 | // is blocked and schedule a new one. 54 | runtime.LockOSThread() 55 | 56 | defer runtime.UnlockOSThread() 57 | defer close(join) 58 | 59 | ticker := time.NewTicker(config.CollectInterval) 60 | config.Collector.Collect() 61 | for { 62 | select { 63 | case <-ticker.C: 64 | config.Collector.Collect() 65 | case <-stop: 66 | return 67 | } 68 | } 69 | }() 70 | 71 | return &closer{stop: stop, join: join} 72 | } 73 | 74 | func setConfigDefaults(config Config) Config { 75 | if config.CollectInterval == 0 { 76 | config.CollectInterval = 15 * time.Second 77 | } 78 | 79 | if config.Collector == nil { 80 | config.Collector = MultiCollector() 81 | } 82 | 83 | return config 84 | } 85 | 86 | type closer struct { 87 | once sync.Once 88 | stop chan<- struct{} 89 | join <-chan struct{} 90 | } 91 | 92 | func (c *closer) Close() error { 93 | c.once.Do(c.close) 94 | return nil 95 | } 96 | 97 | func (c *closer) close() { 98 | close(c.stop) 99 | <-c.join 100 | } 101 | -------------------------------------------------------------------------------- /procstats/collector_test.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | "github.com/segmentio/stats/v5/statstest" 9 | ) 10 | 11 | func TestCollector(t *testing.T) { 12 | h := &statstest.Handler{} 13 | e := stats.NewEngine("", h) 14 | 15 | c := StartCollectorWith(Config{ 16 | CollectInterval: 100 * time.Microsecond, 17 | Collector: MultiCollector( 18 | NewGoMetricsWith(e), 19 | ), 20 | }) 21 | 22 | // Let the collector do a few runs. 23 | time.Sleep(time.Millisecond) 24 | c.Close() 25 | 26 | if len(h.Measures()) == 0 { 27 | t.Error("no measures were reported by the stats collector") 28 | } 29 | 30 | for _, m := range h.Measures() { 31 | t.Log(m) 32 | } 33 | } 34 | 35 | func TestCollectorCloser(t *testing.T) { 36 | c := StartCollector(nil) 37 | 38 | if err := c.Close(); err != nil { 39 | t.Error("unexpected error reported when closing a collector:", err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /procstats/delaystats.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | stats "github.com/segmentio/stats/v5" 8 | ) 9 | 10 | // DelayMetrics is a metric collector that reports resource delays on processes. 11 | type DelayMetrics struct { 12 | engine *stats.Engine 13 | pid int 14 | 15 | CPUDelay time.Duration `metric:"cpu.delay.seconds" type:"counter"` 16 | BlockIODelay time.Duration `metric:"blockio.delay.seconds" type:"counter"` 17 | SwapInDelay time.Duration `metric:"swapin.delay.seconds" type:"counter"` 18 | FreePagesDelay time.Duration `metric:"freepages.delay.seconds" type:"counter"` 19 | } 20 | 21 | // NewDelayMetrics collects metrics on the current process and reports them to 22 | // the default stats engine. 23 | func NewDelayMetrics() *DelayMetrics { 24 | return NewDelayMetricsWith(stats.DefaultEngine, os.Getpid()) 25 | } 26 | 27 | // NewDelayMetricsWith collects metrics on the process identified by pid and 28 | // reports them to eng. 29 | func NewDelayMetricsWith(eng *stats.Engine, pid int) *DelayMetrics { 30 | return &DelayMetrics{engine: eng, pid: pid} 31 | } 32 | 33 | // Collect satisfies the Collector interface. 34 | func (d *DelayMetrics) Collect() { 35 | if info, err := CollectDelayInfo(d.pid); err == nil { 36 | d.CPUDelay = info.CPUDelay 37 | d.BlockIODelay = info.BlockIODelay 38 | d.SwapInDelay = info.SwapInDelay 39 | d.FreePagesDelay = info.FreePagesDelay 40 | d.engine.Report(d) 41 | } 42 | } 43 | 44 | // DelayInfo stores delay Durations for various resources. 45 | type DelayInfo struct { 46 | CPUDelay time.Duration 47 | BlockIODelay time.Duration 48 | SwapInDelay time.Duration 49 | FreePagesDelay time.Duration 50 | } 51 | 52 | // CollectDelayInfo returns DelayInfo for a pid and an error, if any. 53 | func CollectDelayInfo(pid int) (DelayInfo, error) { 54 | return collectDelayInfo(pid) 55 | } 56 | -------------------------------------------------------------------------------- /procstats/delaystats_darwin.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | func collectDelayInfo(_ int) (DelayInfo, error) { 4 | // TODO 5 | return DelayInfo{}, nil 6 | } 7 | -------------------------------------------------------------------------------- /procstats/delaystats_linux.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/mdlayher/taskstats" 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | func collectDelayInfo(pid int) (DelayInfo, error) { 11 | client, err := taskstats.New() 12 | switch { 13 | case errors.Is(err, unix.ENOENT): 14 | return DelayInfo{}, errors.New("failed to communicate with taskstats Netlink family, ensure this program is not running in a network namespace") 15 | case err != nil: 16 | return DelayInfo{}, err 17 | default: 18 | defer client.Close() 19 | } 20 | 21 | stats, err := client.TGID(pid) 22 | switch { 23 | case errors.Is(err, unix.EPERM): 24 | return DelayInfo{}, errors.New("failed to open Netlink socket: permission denied, ensure CAP_NET_RAW is enabled for this process, or run it with root privileges") 25 | case err != nil: 26 | return DelayInfo{}, err 27 | default: 28 | return DelayInfo{ 29 | BlockIODelay: stats.BlockIODelay, 30 | CPUDelay: stats.CPUDelay, 31 | FreePagesDelay: stats.FreePagesDelay, 32 | SwapInDelay: stats.SwapInDelay, 33 | }, nil 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /procstats/delaystats_test.go: -------------------------------------------------------------------------------- 1 | package procstats_test 2 | 3 | import ( 4 | "math/rand" 5 | "os" 6 | "os/user" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/segmentio/stats/v5" 12 | "github.com/segmentio/stats/v5/procstats" 13 | "github.com/segmentio/stats/v5/statstest" 14 | ) 15 | 16 | func TestProcMetrics(t *testing.T) { 17 | u, err := user.Current() 18 | if err != nil || u.Uid != "0" { 19 | t.Log("test needs to be run as root") 20 | t.Skip() 21 | } 22 | 23 | // Create a new random generator 24 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 25 | 26 | h := &statstest.Handler{} 27 | e := stats.NewEngine("", h) 28 | proc := procstats.NewDelayMetricsWith(e, os.Getpid()) 29 | 30 | for i := 0; i != 10; i++ { 31 | tmpFile, err := os.CreateTemp("", "delaystats_test") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | defer func(name string) { 36 | err := os.Remove(name) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | }(tmpFile.Name()) 41 | 42 | b := make([]byte, rng.Int31n(1000000)) 43 | if _, err := rng.Read(b); err != nil { 44 | t.Fatal(err) 45 | } 46 | if _, err := tmpFile.Write(b); err != nil { 47 | t.Fatal(err) 48 | } 49 | if err := tmpFile.Sync(); err != nil { 50 | t.Fatal(err) 51 | } 52 | if err := tmpFile.Close(); err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | t.Logf("collect number %d", i) 57 | 58 | // Work around issues testing on Docker containers that don't use host networking 59 | defer func() { 60 | if r := recover(); r != nil { 61 | if r1, ok := r.(error); ok && strings.HasPrefix(r1.Error(), "Failed to communicate with taskstats Netlink family") { 62 | t.Skip() 63 | return 64 | } 65 | panic(r) 66 | } 67 | }() 68 | proc.Collect() 69 | 70 | if len(h.Measures()) == 0 { 71 | t.Error("no measures were reported by the stats collector") 72 | } 73 | 74 | for _, m := range h.Measures() { 75 | t.Log(m) 76 | } 77 | 78 | h.Clear() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /procstats/delaystats_windows.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | func collectDelayInfo(_ int) (DelayInfo, error) { 4 | // TODO 5 | return DelayInfo{}, nil 6 | } 7 | -------------------------------------------------------------------------------- /procstats/error.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | import "fmt" 4 | 5 | func convertPanicToError(v interface{}) (err error) { 6 | if v != nil { 7 | switch e := v.(type) { 8 | case error: 9 | err = e 10 | default: 11 | err = fmt.Errorf("%v", e) 12 | } 13 | } 14 | return 15 | } 16 | 17 | func check(err error) { 18 | if err != nil { 19 | panic(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /procstats/error_test.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestConvertPanicToError(t *testing.T) { 11 | tests := []struct { 12 | v interface{} 13 | e error 14 | }{ 15 | { 16 | v: nil, 17 | e: nil, 18 | }, 19 | { 20 | v: io.EOF, 21 | e: io.EOF, 22 | }, 23 | { 24 | v: "hello world", 25 | e: errors.New("hello world"), 26 | }, 27 | } 28 | 29 | for _, test := range tests { 30 | if err := convertPanicToError(test.v); !reflect.DeepEqual(err, test.e) { 31 | t.Errorf("bad error from panic: %v != %v", test.e, err) 32 | } 33 | } 34 | } 35 | 36 | func TestCheck(t *testing.T) { 37 | err := io.EOF 38 | 39 | defer func() { 40 | if x := recover(); x != err { 41 | t.Error("invalid panic:", x) 42 | } 43 | }() 44 | 45 | check(err) 46 | } 47 | -------------------------------------------------------------------------------- /procstats/go_test.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | "time" 7 | 8 | stats "github.com/segmentio/stats/v5" 9 | "github.com/segmentio/stats/v5/statstest" 10 | ) 11 | 12 | func TestGoMetrics(t *testing.T) { 13 | h := &statstest.Handler{} 14 | e := stats.NewEngine("", h) 15 | 16 | gostats := NewGoMetricsWith(e) 17 | 18 | for i := 0; i != 10; i++ { 19 | t.Logf("collect number %d", i) 20 | gostats.Collect() 21 | 22 | if len(h.Measures()) == 0 { 23 | t.Error("no measures were reported by the stats collector") 24 | } 25 | 26 | for _, m := range h.Measures() { 27 | t.Log(m) 28 | } 29 | 30 | h.Clear() 31 | 32 | for j := 0; j <= i; j++ { 33 | runtime.GC() // to get non-zero GC stats 34 | } 35 | 36 | time.Sleep(10 * time.Millisecond) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /procstats/linux/cgroup.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | // ProcCGroup is a type alias for a []CGroup. 9 | type ProcCGroup []CGroup 10 | 11 | // CGroup holds configuration information for a Linux cgroup. 12 | type CGroup struct { 13 | ID int 14 | Name string 15 | Path string // Path in /sys/fs/cgroup 16 | } 17 | 18 | // Lookup takes a string argument representing the name of a Linux cgroup 19 | // and returns a CGroup and bool indicating whether or not the cgroup was found. 20 | func (pcg ProcCGroup) Lookup(name string) (cgroup CGroup, ok bool) { 21 | forEachToken(name, ",", func(key1 string) { 22 | for _, cg := range pcg { 23 | forEachToken(cg.Name, ",", func(key2 string) { 24 | if key1 == key2 { 25 | cgroup, ok = cg, true 26 | } 27 | }) 28 | } 29 | }) 30 | return 31 | } 32 | 33 | // ReadProcCGroup takes an int argument representing a PID 34 | // and returns a ProcCGroup and error, if any is encountered. 35 | func ReadProcCGroup(pid int) (proc ProcCGroup, err error) { 36 | defer func() { err = convertPanicToError(recover()) }() 37 | proc = parseProcCGroup(readProcFile(pid, "cgroup")) 38 | return 39 | } 40 | 41 | // ParseProcCGroup parses Linux system cgroup data and returns a ProcCGroup and error, if any is encountered. 42 | func ParseProcCGroup(s string) (proc ProcCGroup, err error) { 43 | defer func() { err = convertPanicToError(recover()) }() 44 | proc = parseProcCGroup(s) 45 | return 46 | } 47 | 48 | func parseProcCGroup(s string) (proc ProcCGroup) { 49 | forEachLine(s, func(line string) { 50 | id, line := split(line, ':') 51 | name, path := split(line, ':') 52 | 53 | for len(name) != 0 { 54 | var next string 55 | name, next = split(name, ',') 56 | 57 | if strings.HasPrefix(name, "name=") { // WTF? 58 | name = strings.TrimSpace(name[5:]) 59 | } 60 | 61 | proc = append(proc, CGroup{ID: atoi(id), Name: name, Path: path}) 62 | name = next 63 | } 64 | }) 65 | return 66 | } 67 | 68 | // ReadCPUPeriod takes a string representing a Linux cgroup and returns 69 | // the period as a time.Duration that is applied for this cgroup and an error, if any. 70 | func ReadCPUPeriod(cgroup string) (period time.Duration, err error) { 71 | defer func() { err = convertPanicToError(recover()) }() 72 | period = readCPUPeriod(cgroup) 73 | return 74 | } 75 | 76 | // ReadCPUQuota takes a string representing a Linux cgroup and returns 77 | // the quota as a time.Duration that is applied for this cgroup and an error, if any. 78 | func ReadCPUQuota(cgroup string) (quota time.Duration, err error) { 79 | defer func() { err = convertPanicToError(recover()) }() 80 | quota = readCPUQuota(cgroup) 81 | return 82 | } 83 | 84 | // ReadCPUShares takes a string representing a Linux cgroup and returns 85 | // an int64 representing the cpu shares allotted for this cgroup and an error, if any. 86 | func ReadCPUShares(cgroup string) (shares int64, err error) { 87 | defer func() { err = convertPanicToError(recover()) }() 88 | shares = readCPUShares(cgroup) 89 | return 90 | } 91 | 92 | func readCPUPeriod(cgroup string) time.Duration { 93 | return readMicrosecondFile(cgroupPath("cpu", cgroup, "cpu.cfs_period_us")) 94 | } 95 | 96 | func readCPUQuota(cgroup string) time.Duration { 97 | return readMicrosecondFile(cgroupPath("cpu", cgroup, "cpu.cfs_quota_us")) 98 | } 99 | 100 | func readCPUShares(cgroup string) int64 { 101 | return readIntFile(cgroupPath("cpu", cgroup, "cpu.shares")) 102 | } 103 | -------------------------------------------------------------------------------- /procstats/linux/cgroup_darwin_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadProcCGroup(t *testing.T) { 9 | if _, err := ReadProcCGroup(os.Getpid()); err == nil { 10 | t.Error("ReadProcCGroup should have failed on Darwin") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /procstats/linux/cgroup_linux_test.go: -------------------------------------------------------------------------------- 1 | // This is a build tag hack to permit the test suite 2 | // to succeed on the ubuntu-latest runner (linux-amd64), 3 | // which apparently no longer has /sys/fs/cgroup/cpu/* files. 4 | // 5 | //go:build linux && arm64 6 | 7 | package linux 8 | 9 | import ( 10 | "os" 11 | "reflect" 12 | "testing" 13 | ) 14 | 15 | func TestParseProcCGroup(t *testing.T) { 16 | text := `11:memory:/user.slice 17 | 10:pids:/user.slice/user-1000.slice 18 | 9:devices:/user.slice 19 | 8:net_cls,net_prio:/ 20 | 7:blkio:/user.slice 21 | 6:hugetlb:/ 22 | 5:cpu,cpuacct:/user.slice 23 | 4:perf_event:/ 24 | 3:freezer:/ 25 | 2:cpuset:/ 26 | 1:name=systemd:/user.slice/user-1000.slice/session-3925.scope 27 | ` 28 | 29 | proc, err := ParseProcCGroup(text) 30 | 31 | if err != nil { 32 | t.Error(err) 33 | return 34 | } 35 | 36 | if !reflect.DeepEqual(proc, ProcCGroup{ 37 | {11, "memory", "/user.slice"}, 38 | {10, "pids", "/user.slice/user-1000.slice"}, 39 | {9, "devices", "/user.slice"}, 40 | {8, "net_cls", "/"}, 41 | {8, "net_prio", "/"}, 42 | {7, "blkio", "/user.slice"}, 43 | {6, "hugetlb", "/"}, 44 | {5, "cpu", "/user.slice"}, 45 | {5, "cpuacct", "/user.slice"}, 46 | {4, "perf_event", "/"}, 47 | {3, "freezer", "/"}, 48 | {2, "cpuset", "/"}, 49 | {1, "systemd", "/user.slice/user-1000.slice/session-3925.scope"}, 50 | }) { 51 | t.Error(proc) 52 | } 53 | } 54 | 55 | func sysGone(t *testing.T) bool { 56 | t.Helper() 57 | _, err := os.Stat("/sys/fs/cgroup/cpu/cpu.cfs_period_us") 58 | return os.IsNotExist(err) 59 | } 60 | 61 | func TestProcCGroupLookup(t *testing.T) { 62 | tests := []struct { 63 | proc ProcCGroup 64 | name string 65 | cgroup CGroup 66 | }{ 67 | { 68 | proc: ProcCGroup{{1, "A", "/"}, {2, "B", "/"}, {2, "C", "/"}}, 69 | name: "", 70 | }, 71 | { 72 | proc: ProcCGroup{{1, "A", "/"}, {2, "B", "/"}, {2, "C", "/"}}, 73 | name: "A", 74 | cgroup: CGroup{1, "A", "/"}, 75 | }, 76 | { 77 | proc: ProcCGroup{{1, "A", "/"}, {2, "B", "/"}, {2, "C", "/"}}, 78 | name: "B", 79 | cgroup: CGroup{2, "B", "/"}, 80 | }, 81 | } 82 | 83 | for _, test := range tests { 84 | if cgroup, _ := test.proc.Lookup(test.name); !reflect.DeepEqual(cgroup, test.cgroup) { 85 | t.Errorf("bad cgroups from name %v: %+v != %+v", test.name, test.cgroup, cgroup) 86 | } 87 | } 88 | } 89 | 90 | func TestReadCPUPeriod(t *testing.T) { 91 | if sysGone(t) { 92 | t.Skip("/sys files not available on this filesystem; skipping test") 93 | } 94 | period, err := ReadCPUPeriod("") 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | if period == 0 { 99 | t.Fatal("invalid CPU period:", period) 100 | } 101 | } 102 | 103 | func TestReadCPUQuota(t *testing.T) { 104 | if sysGone(t) { 105 | t.Skip("/sys files not available on this filesystem; skipping test") 106 | } 107 | quota, err := ReadCPUQuota("") 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | if quota == 0 { 112 | t.Fatal("invalid CPU quota:", quota) 113 | } 114 | } 115 | 116 | func TestReadCPUShares(t *testing.T) { 117 | if sysGone(t) { 118 | t.Skip("/sys files not available on this filesystem; skipping test") 119 | } 120 | shares, err := ReadCPUShares("") 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | if shares == 0 { 125 | t.Fatal("invalid CPU shares:", shares) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /procstats/linux/error.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import "fmt" 4 | 5 | func convertPanicToError(v interface{}) (err error) { 6 | if v != nil { 7 | switch e := v.(type) { 8 | case error: 9 | err = e 10 | default: 11 | err = fmt.Errorf("%v", e) 12 | } 13 | } 14 | return 15 | } 16 | 17 | func check(err error) { 18 | if err != nil { 19 | panic(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /procstats/linux/error_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestConvertPanicToError(t *testing.T) { 11 | tests := []struct { 12 | v interface{} 13 | e error 14 | }{ 15 | { 16 | v: nil, 17 | e: nil, 18 | }, 19 | { 20 | v: io.EOF, 21 | e: io.EOF, 22 | }, 23 | { 24 | v: "hello world", 25 | e: errors.New("hello world"), 26 | }, 27 | } 28 | 29 | for _, test := range tests { 30 | if err := convertPanicToError(test.v); !reflect.DeepEqual(err, test.e) { 31 | t.Errorf("bad error from panic: %v != %v", test.e, err) 32 | } 33 | } 34 | } 35 | 36 | func TestCheck(t *testing.T) { 37 | err := io.EOF 38 | 39 | defer func() { 40 | if x := recover(); x != err { 41 | t.Error("invalid panic:", x) 42 | } 43 | }() 44 | 45 | check(err) 46 | } 47 | -------------------------------------------------------------------------------- /procstats/linux/files.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import "os" 4 | 5 | // ReadOpenFileCount takes an int representing a PID and 6 | // returns a uint64 representing the open file descriptor count 7 | // for this process and an error, if any. 8 | func ReadOpenFileCount(pid int) (n uint64, err error) { 9 | defer func() { err = convertPanicToError(recover()) }() 10 | n = readOpenFileCount(pid) 11 | return 12 | } 13 | 14 | func readOpenFileCount(pid int) uint64 { 15 | f, err := os.Open(procPath(pid, "fd")) 16 | check(err) 17 | defer f.Close() 18 | 19 | // May not be the most efficient way to do this, but the little dance 20 | // for getting open/readdirent/parsedirent is gonna take a while to get 21 | // right. 22 | s, err := f.Readdirnames(-1) 23 | check(err) 24 | return uint64(len(s)) 25 | } 26 | -------------------------------------------------------------------------------- /procstats/linux/files_darwin_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadOpenFileCount(t *testing.T) { 9 | if _, err := ReadOpenFileCount(os.Getpid()); err == nil { 10 | t.Error("ReadOpenFileCount should have failed on Darwin") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /procstats/linux/files_linux_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadOpenFileCount(t *testing.T) { 9 | if count, err := ReadOpenFileCount(os.Getpid()); err != nil { 10 | t.Error("ReadOpenFileCount:", err) 11 | } else if count == 0 { 12 | t.Error("ReadOpenFileCount: cannot return zero") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /procstats/linux/io.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func readFile(path string) string { 13 | b, err := os.ReadFile(path) 14 | check(err) 15 | return string(b) 16 | } 17 | 18 | func readProcFile(who interface{}, what string) string { 19 | return readFile(procPath(who, what)) 20 | } 21 | 22 | func readMicrosecondFile(path string) time.Duration { 23 | return time.Duration(readIntFile(path)) * time.Microsecond 24 | } 25 | 26 | func readIntFile(path string) int64 { 27 | return parseInt(strings.TrimSpace(readFile(path))) 28 | } 29 | 30 | func parseInt(s string) int64 { 31 | i, err := strconv.ParseInt(s, 10, 64) 32 | check(err) 33 | return i 34 | } 35 | 36 | func procPath(who interface{}, what string) string { 37 | return filepath.Join("/proc", fmt.Sprint(who), what) 38 | } 39 | 40 | func cgroupPath(dir, cgroup, file string) string { 41 | return filepath.Join("/sys/fs/cgroup", dir, cgroup, file) 42 | } 43 | -------------------------------------------------------------------------------- /procstats/linux/io_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadFile(t *testing.T) { 9 | f, err := os.CreateTemp("/tmp", "io_test_") 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | path := f.Name() 14 | if _, err := f.Write([]byte("Hello World!\n")); err != nil { 15 | t.Error(err) 16 | } 17 | if err := f.Close(); err != nil { 18 | t.Error(err) 19 | } 20 | if s := readFile(path); s != "Hello World!\n" { 21 | t.Error("invalid file content:", s) 22 | } 23 | } 24 | 25 | func TestReadProcFilePanic(t *testing.T) { 26 | defer func() { recover() }() 27 | readProcFile("asdfasdf", "asdfasdf") // doesn't exist 28 | t.Error("should have raised a panic") 29 | } 30 | -------------------------------------------------------------------------------- /procstats/linux/limits.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import "strconv" 4 | 5 | // Represents Linux's unlimited for resource limits. 6 | const ( 7 | Unlimited uint64 = 1<<64 - 1 8 | ) 9 | 10 | // Limits holds configuration for resource limits. 11 | type Limits struct { 12 | Name string 13 | Soft uint64 14 | Hard uint64 15 | Unit string 16 | } 17 | 18 | // ProcLimits holds Limits for processes. 19 | type ProcLimits struct { 20 | CPUTime Limits // seconds 21 | FileSize Limits // bytes 22 | DataSize Limits // bytes 23 | StackSize Limits // bytes 24 | CoreFileSize Limits // bytes 25 | ResidentSet Limits // bytes 26 | Processes Limits // processes 27 | OpenFiles Limits // files 28 | LockedMemory Limits // bytes 29 | AddressSpace Limits // bytes 30 | FileLocks Limits // locks 31 | PendingSignals Limits // signals 32 | MsgqueueSize Limits // bytes 33 | NicePriority Limits 34 | RealtimePriority Limits 35 | RealtimeTimeout Limits 36 | } 37 | 38 | // ReadProcLimits returns the ProcLimits and an error, if any, for a PID. 39 | func ReadProcLimits(pid int) (proc ProcLimits, err error) { 40 | defer func() { err = convertPanicToError(recover()) }() 41 | proc = parseProcLimits(readProcFile(pid, "limits")) 42 | return 43 | } 44 | 45 | // ParseProcLimits parses system process limits and returns a ProcLimits and error, if any. 46 | func ParseProcLimits(s string) (proc ProcLimits, err error) { 47 | defer func() { err = convertPanicToError(recover()) }() 48 | proc = parseProcLimits(s) 49 | return 50 | } 51 | 52 | func parseProcLimits(s string) (proc ProcLimits) { 53 | index := map[string]*Limits{ 54 | "Max cpu time": &proc.CPUTime, 55 | "Max file size": &proc.FileSize, 56 | "Max data size": &proc.DataSize, 57 | "Max stack size": &proc.StackSize, 58 | "Max core file size": &proc.CoreFileSize, 59 | "Max resident set": &proc.ResidentSet, 60 | "Max processes": &proc.Processes, 61 | "Max open files": &proc.OpenFiles, 62 | "Max locked memory": &proc.LockedMemory, 63 | "Max address space": &proc.AddressSpace, 64 | "Max file locks": &proc.FileLocks, 65 | "Max pending signals": &proc.PendingSignals, 66 | "Max msgqueue size": &proc.MsgqueueSize, 67 | "Max nice priority": &proc.NicePriority, 68 | "Max realtime priority": &proc.RealtimePriority, 69 | "Max realtime timeout": &proc.RealtimeTimeout, 70 | } 71 | 72 | columns := make([]string, 0, 4) 73 | forEachLineExceptFirst(s, func(line string) { 74 | columns = columns[:0] 75 | forEachColumn(line, func(col string) { columns = append(columns, col) }) 76 | 77 | var limits Limits 78 | length := len(columns) 79 | 80 | if length > 0 { 81 | limits.Name = columns[0] 82 | } 83 | 84 | if length > 1 { 85 | limits.Soft = parseLimitUint(columns[1]) 86 | } 87 | 88 | if length > 2 { 89 | limits.Hard = parseLimitUint(columns[2]) 90 | } 91 | 92 | if length > 3 { 93 | limits.Unit = columns[3] 94 | } 95 | 96 | if ptr := index[limits.Name]; ptr != nil { 97 | *ptr = limits 98 | } 99 | }) 100 | 101 | return 102 | } 103 | 104 | func parseLimitUint(s string) uint64 { 105 | if s == "unlimited" { 106 | return Unlimited 107 | } 108 | v, e := strconv.ParseUint(s, 10, 64) 109 | check(e) 110 | return v 111 | } 112 | -------------------------------------------------------------------------------- /procstats/linux/limits_darwin_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadProcLimits(t *testing.T) { 9 | if _, err := ReadProcLimits(os.Getpid()); err == nil { 10 | t.Error("ReadProcLimits should have failed on Darwin") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /procstats/linux/limits_linux_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadProcLimits(t *testing.T) { 9 | if limits, err := ReadProcLimits(os.Getpid()); err != nil { 10 | t.Error("ReadProcLimits:", err) 11 | } else if limits.CPUTime.Hard == 0 { 12 | t.Error("ReadProcLimits: hard cpu time limit cannot be zero") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /procstats/linux/limits_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseProcLimits(t *testing.T) { 9 | text := `Limit Soft Limit Hard Limit Units 10 | Max cpu time unlimited unlimited seconds 11 | Max file size unlimited unlimited bytes 12 | Max data size unlimited unlimited bytes 13 | Max stack size 8388608 unlimited bytes 14 | Max core file size 0 unlimited bytes 15 | Max resident set unlimited unlimited bytes 16 | Max processes unlimited unlimited processes 17 | Max open files 1048576 1048576 files 18 | Max locked memory 65536 65536 bytes 19 | Max address space unlimited unlimited bytes 20 | Max file locks unlimited unlimited locks 21 | Max pending signals 7767 7767 signals 22 | Max msgqueue size 819200 819200 bytes 23 | Max nice priority 0 0 24 | Max realtime priority 0 0 25 | Max realtime timeout unlimited unlimited us 26 | ` 27 | 28 | proc, err := ParseProcLimits(text) 29 | if err != nil { 30 | t.Error(err) 31 | return 32 | } 33 | 34 | if !reflect.DeepEqual(proc, ProcLimits{ 35 | CPUTime: Limits{"Max cpu time", Unlimited, Unlimited, "seconds"}, 36 | FileSize: Limits{"Max file size", Unlimited, Unlimited, "bytes"}, 37 | DataSize: Limits{"Max data size", Unlimited, Unlimited, "bytes"}, 38 | StackSize: Limits{"Max stack size", 8388608, Unlimited, "bytes"}, 39 | CoreFileSize: Limits{"Max core file size", 0, Unlimited, "bytes"}, 40 | ResidentSet: Limits{"Max resident set", Unlimited, Unlimited, "bytes"}, 41 | Processes: Limits{"Max processes", Unlimited, Unlimited, "processes"}, 42 | OpenFiles: Limits{"Max open files", 1048576, 1048576, "files"}, 43 | LockedMemory: Limits{"Max locked memory", 65536, 65536, "bytes"}, 44 | AddressSpace: Limits{"Max address space", Unlimited, Unlimited, "bytes"}, 45 | FileLocks: Limits{"Max file locks", Unlimited, Unlimited, "locks"}, 46 | PendingSignals: Limits{"Max pending signals", 7767, 7767, "signals"}, 47 | MsgqueueSize: Limits{"Max msgqueue size", 819200, 819200, "bytes"}, 48 | NicePriority: Limits{"Max nice priority", 0, 0, ""}, 49 | RealtimePriority: Limits{"Max realtime priority", 0, 0, ""}, 50 | RealtimeTimeout: Limits{"Max realtime timeout", Unlimited, Unlimited, "us"}, 51 | }) { 52 | t.Error(proc) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /procstats/linux/memory.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | const ( 4 | unlimitedMemoryLimit = 9223372036854771712 5 | ) 6 | 7 | // ReadMemoryLimit returns the memory limit and an error, if any, for a PID. 8 | func ReadMemoryLimit(pid int) (limit uint64, err error) { return readMemoryLimit(pid) } 9 | -------------------------------------------------------------------------------- /procstats/linux/memory_darwin.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | func readMemoryLimit(_ int) (limit uint64, err error) { 4 | limit = unlimitedMemoryLimit 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /procstats/linux/memory_darwin_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadMemoryLimit(t *testing.T) { 9 | limit, err := ReadMemoryLimit(os.Getpid()) 10 | if err != nil || limit != unlimitedMemoryLimit { 11 | t.Error("memory should be unlimited on darwin") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /procstats/linux/memory_linux.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strconv" 7 | "strings" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func readMemoryLimit(pid int) (limit uint64, err error) { 13 | if limit = readCGroupMemoryLimit(pid); limit == unlimitedMemoryLimit { 14 | limit, err = readSysinfoMemoryLimit() 15 | } 16 | return 17 | } 18 | 19 | func readCGroupMemoryLimit(pid int) (limit uint64) { 20 | if cgroups, err := ReadProcCGroup(pid); err == nil { 21 | limit = readProcCGroupMemoryLimit(cgroups) 22 | } 23 | return 24 | } 25 | 26 | func readProcCGroupMemoryLimit(cgroups ProcCGroup) (limit uint64) { 27 | if memory, ok := cgroups.Lookup("memory"); ok { 28 | limit = readMemoryCGroupMemoryLimit(memory) 29 | } 30 | return 31 | } 32 | 33 | func readMemoryCGroupMemoryLimit(cgroup CGroup) (limit uint64) { 34 | limit = unlimitedMemoryLimit // default value if something doesn't work 35 | 36 | if b, err := os.ReadFile(readMemoryCGroupMemoryLimitFilePath(cgroup.Path)); err == nil { 37 | if v, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64); err == nil { 38 | limit = v 39 | } 40 | } 41 | 42 | return 43 | } 44 | 45 | func readMemoryCGroupMemoryLimitFilePath(cgroupPath string) string { 46 | path := "/sys/fs/cgroup/memory" 47 | 48 | // Docker generates weird cgroup paths that don't really exist on the file 49 | // system. 50 | if !strings.HasPrefix(cgroupPath, "/docker/") { 51 | path = filepath.Join(path, cgroupPath) 52 | } 53 | 54 | return filepath.Join(path, "memory.limit_in_bytes") 55 | } 56 | 57 | func readSysinfoMemoryLimit() (limit uint64, err error) { 58 | var sysinfo unix.Sysinfo_t 59 | 60 | if err = unix.Sysinfo(&sysinfo); err == nil { 61 | // unix.Sysinfo returns an uint32 on linux/arm, but uint64 otherwise 62 | limit = uint64(sysinfo.Unit) * uint64(sysinfo.Totalram) 63 | } 64 | 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /procstats/linux/memory_linux_test.go: -------------------------------------------------------------------------------- 1 | // This is a build tag hack to permit the test suite 2 | // to succeed on the ubuntu-latest runner (linux-amd64), 3 | // which apparently no longer succeeds with the included test. 4 | // 5 | //go:build linux && arm64 6 | 7 | package linux 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func TestReadMemoryLimit(t *testing.T) { 15 | if sysGone(t) { 16 | t.Skip("/sys files not available on this filesystem; skipping test") 17 | } 18 | if limit, err := ReadMemoryLimit(os.Getpid()); err != nil { 19 | t.Error(err) 20 | 21 | } else if limit == 0 { 22 | t.Error("memory should not be zero") 23 | 24 | } else if limit == unlimitedMemoryLimit { 25 | t.Error("memory should not be unlimited") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /procstats/linux/parse.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | func forEachToken(text, split string, call func(string)) { 10 | for len(text) != 0 { 11 | var line string 12 | 13 | if i := strings.Index(text, split); i >= 0 { 14 | line, text = text[:i], text[i+len(split):] 15 | } else { 16 | line, text = text, "" 17 | } 18 | 19 | call(line) 20 | } 21 | } 22 | 23 | func forEachLine(text string, call func(string)) { 24 | forEachToken(text, "\n", func(line string) { 25 | if line = strings.TrimSpace(line); line != "" { 26 | call(line) 27 | } 28 | }) 29 | } 30 | 31 | func forEachLineExceptFirst(text string, call func(string)) { 32 | first := true 33 | forEachLine(text, func(line string) { 34 | if first { 35 | first = false 36 | } else { 37 | call(line) 38 | } 39 | }) 40 | } 41 | 42 | func forEachColumn(line string, call func(string)) { 43 | for line = skipSpaces(line); len(line) != 0; line = skipSpaces(line) { 44 | var column string 45 | 46 | if i := strings.Index(line, " "); i >= 0 { 47 | column, line = line[:i], line[i+2:] 48 | } else { 49 | column, line = line, "" 50 | } 51 | 52 | call(column) 53 | } 54 | } 55 | 56 | func forEachProperty(text string, call func(string, string)) { 57 | forEachLine(text, func(line string) { call(splitProperty(line)) }) 58 | } 59 | 60 | func splitProperty(text string) (key, val string) { 61 | return split(text, ':') 62 | } 63 | 64 | func split(text string, sep byte) (head, tail string) { 65 | if i := strings.IndexByte(text, sep); i >= 0 { 66 | head, tail = text[:i], text[i+1:] 67 | } else { 68 | head = text 69 | } 70 | head = strings.TrimSpace(head) 71 | tail = strings.TrimSpace(tail) 72 | return 73 | } 74 | 75 | func skipSpaces(text string) string { 76 | for i, c := range text { 77 | if !unicode.IsSpace(c) { 78 | return text[i:] 79 | } 80 | } 81 | return "" 82 | } 83 | 84 | func skipLine(text string) string { 85 | if i := strings.IndexByte(text, '\n'); i >= 0 { 86 | return text[i+1:] 87 | } 88 | return "" 89 | } 90 | 91 | func atoi(s string) int { 92 | v, e := strconv.Atoi(s) 93 | check(e) 94 | return v 95 | } 96 | -------------------------------------------------------------------------------- /procstats/linux/parse_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestForEachLine(t *testing.T) { 9 | tests := []struct { 10 | text string 11 | lines []string 12 | }{ 13 | { 14 | text: "", 15 | lines: []string{}, 16 | }, 17 | { 18 | text: "1\n2\n3\nHello \n World!", 19 | lines: []string{"1", "2", "3", "Hello", "World!"}, 20 | }, 21 | } 22 | 23 | for _, test := range tests { 24 | lines := []string{} 25 | forEachLine(test.text, func(line string) { lines = append(lines, line) }) 26 | 27 | if !reflect.DeepEqual(lines, test.lines) { 28 | t.Error(lines) 29 | } 30 | } 31 | } 32 | 33 | func TestForEachColumn(t *testing.T) { 34 | tests := []struct { 35 | text string 36 | columns []string 37 | }{ 38 | { 39 | text: "", 40 | columns: []string{}, 41 | }, 42 | { 43 | text: "Hello World! A B C", 44 | columns: []string{"Hello World!", "A", "B", "C"}, 45 | }, 46 | { 47 | text: "Max cpu time unlimited unlimited seconds", 48 | columns: []string{"Max cpu time", "unlimited", "unlimited", "seconds"}, 49 | }, 50 | } 51 | 52 | for _, test := range tests { 53 | columns := []string{} 54 | forEachColumn(test.text, func(column string) { columns = append(columns, column) }) 55 | 56 | if !reflect.DeepEqual(columns, test.columns) { 57 | t.Error(columns) 58 | } 59 | } 60 | } 61 | 62 | func TestForEachProperty(t *testing.T) { 63 | type KV struct { 64 | K string 65 | V string 66 | } 67 | 68 | tests := []struct { 69 | text string 70 | kv []KV 71 | }{ 72 | { 73 | text: "", 74 | kv: []KV{}, 75 | }, 76 | { 77 | text: "A: 1\nB: 2\nC: 3\nD", 78 | kv: []KV{{"A", "1"}, {"B", "2"}, {"C", "3"}, {"D", ""}}, 79 | }, 80 | } 81 | 82 | for _, test := range tests { 83 | kv := []KV{} 84 | forEachProperty(test.text, func(k, v string) { kv = append(kv, KV{k, v}) }) 85 | 86 | if !reflect.DeepEqual(kv, test.kv) { 87 | t.Error(kv) 88 | } 89 | } 90 | } 91 | 92 | func TestSkipLine(t *testing.T) { 93 | tests := []struct { 94 | s1 string 95 | s2 string 96 | }{ 97 | { 98 | s1: "", 99 | s2: "", 100 | }, 101 | { 102 | s1: "Hello World!", 103 | s2: "", 104 | }, 105 | { 106 | s1: "Hello\nWorld\n", 107 | s2: "World\n", 108 | }, 109 | } 110 | 111 | for _, test := range tests { 112 | if s := skipLine(test.s1); s != test.s2 { 113 | t.Errorf("skipLine(%#v) => %#v != %#v", test.s1, test.s2, s) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /procstats/linux/sched.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import "strconv" 4 | 5 | // ProcSched contains statistics about process scheduling, utilization, and switches. 6 | type ProcSched struct { 7 | NRSwitches uint64 // nr_switches 8 | NRVoluntarySwitches uint64 // nr_voluntary_switches 9 | NRInvoluntarySwitches uint64 // nr_involuntary_switches 10 | SEAvgLoadSum uint64 // se.avg.load_sum 11 | SEAvgUtilSum uint64 // se.avg.util_sum 12 | SEAvgLoadAvg uint64 // se.avg.load_avg 13 | SEAvgUtilAvg uint64 // se.avg.util_avg 14 | } 15 | 16 | // ReadProcSched returns a ProcSched and error, if any, for a PID. 17 | func ReadProcSched(pid int) (proc ProcSched, err error) { 18 | defer func() { err = convertPanicToError(recover()) }() 19 | proc = parseProcSched(readProcFile(pid, "sched")) 20 | return 21 | } 22 | 23 | // ParseProcSched processes system process scheduling data and returns a ProcSched and error, if any. 24 | func ParseProcSched(s string) (proc ProcSched, err error) { 25 | defer func() { err = convertPanicToError(recover()) }() 26 | proc = parseProcSched(s) 27 | return 28 | } 29 | 30 | func parseProcSched(s string) (proc ProcSched) { 31 | intFields := map[string]*uint64{ 32 | "nr_switches": &proc.NRSwitches, 33 | "nr_voluntary_switches": &proc.NRVoluntarySwitches, 34 | "nr_involuntary_switches": &proc.NRInvoluntarySwitches, 35 | "se.avg.load_sum": &proc.SEAvgLoadSum, 36 | "se.avg.util_sum": &proc.SEAvgUtilSum, 37 | "se.avg.load_avg": &proc.SEAvgLoadAvg, 38 | "se.avg.util_avg": &proc.SEAvgUtilAvg, 39 | } 40 | 41 | s = skipLine(s) // (, #threads: 1) 42 | s = skipLine(s) // ------------------------------- 43 | 44 | forEachProperty(s, func(key, val string) { 45 | if field := intFields[key]; field != nil { 46 | v, e := strconv.ParseUint(val, 10, 64) 47 | check(e) 48 | 49 | // There's no much doc on the structure of this file, unsure if 50 | // there would be a breakdown per thread... unlikely but this should 51 | // cover for it. 52 | *field += v 53 | } 54 | }) 55 | 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /procstats/linux/sched_darwin_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadProcSched(t *testing.T) { 9 | if _, err := ReadProcSched(os.Getpid()); err == nil { 10 | t.Error("ReadProcSched should have failed on Darwin") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /procstats/linux/sched_linux_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadProcSched(t *testing.T) { 9 | if _, err := ReadProcSched(os.Getpid()); err != nil { 10 | t.Error("ReadProcSched:", err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /procstats/linux/sched_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseProcSched(t *testing.T) { 9 | text := `cat (5013, #threads: 1) 10 | ------------------------------------------------------------------- 11 | se.exec_start : 44212723.563800 12 | se.vruntime : 0.553868 13 | se.sum_exec_runtime : 8.945468 14 | nr_switches : 51 15 | nr_voluntary_switches : 45 16 | nr_involuntary_switches : 6 17 | se.load.weight : 1024 18 | se.avg.load_sum : 2139396 19 | se.avg.util_sum : 2097665 20 | se.avg.load_avg : 40 21 | se.avg.util_avg : 39 22 | se.avg.last_update_time : 44212723563800 23 | policy : 0 24 | prio : 120 25 | clock-delta : 41 26 | ` 27 | 28 | proc, err := ParseProcSched(text) 29 | if err != nil { 30 | t.Error(err) 31 | return 32 | } 33 | 34 | if !reflect.DeepEqual(proc, ProcSched{ 35 | NRSwitches: 51, 36 | NRVoluntarySwitches: 45, 37 | NRInvoluntarySwitches: 6, 38 | SEAvgLoadSum: 2139396, 39 | SEAvgUtilSum: 2097665, 40 | SEAvgLoadAvg: 40, 41 | SEAvgUtilAvg: 39, 42 | }) { 43 | t.Error(proc) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /procstats/linux/stat_darwin_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadProcStat(t *testing.T) { 9 | if _, err := ReadProcStat(os.Getpid()); err == nil { 10 | t.Error("ReadProcStat should have failed on Darwin") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /procstats/linux/stat_linux_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadProcStat(t *testing.T) { 9 | if stat, err := ReadProcStat(os.Getpid()); err != nil { 10 | t.Error("ReadProcStat:", err) 11 | } else if int(stat.Pid) != os.Getpid() { 12 | t.Error("ReadPorcStat: invalid pid returned:", stat.Pid) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /procstats/linux/stat_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseProcStat(t *testing.T) { 9 | text := `69 (cat) R 56 1 1 0 -1 4210944 83 0 0 0 0 0 0 0 20 0 1 0 1977676 4644864 193 18446744073709551615 4194304 4240332 140724300789216 140724300788568 140342654634416 0 0 0 0 0 0 0 17 0 0 0 0 0 0 6340112 6341364 24690688 140724300791495 140724300791515 140724300791515 140724300791791 0` 10 | 11 | proc, err := ParseProcStat(text) 12 | if err != nil { 13 | t.Error(err) 14 | return 15 | } 16 | 17 | if !reflect.DeepEqual(proc, ProcStat{ 18 | Pid: 69, 19 | Comm: "(cat)", 20 | State: Running, 21 | Ppid: 56, 22 | Pgrp: 1, 23 | Session: 1, 24 | TTY: 0, 25 | Tpgid: -1, 26 | Flags: 4210944, 27 | Minflt: 83, 28 | Cminflt: 0, 29 | Majflt: 0, 30 | Cmajflt: 0, 31 | Utime: 0, 32 | Stime: 0, 33 | Cutime: 0, 34 | Cstime: 0, 35 | Priority: 20, 36 | Nice: 0, 37 | NumThreads: 1, 38 | Itrealvalue: 0, 39 | Starttime: 1977676, 40 | Vsize: 4644864, 41 | Rss: 193, 42 | Rsslim: 18446744073709551615, 43 | Startcode: 4194304, 44 | Endcode: 4240332, 45 | Startstack: 140724300789216, 46 | Kstkeep: 140724300788568, 47 | Kstkeip: 140342654634416, 48 | Signal: 0, 49 | Blocked: 0, 50 | Sigignore: 0, 51 | Sigcatch: 0, 52 | Wchan: 0, 53 | Nswap: 0, 54 | Cnswap: 0, 55 | ExitSignal: 17, 56 | Processor: 0, 57 | RTPriority: 0, 58 | Policy: 0, 59 | DelayacctBlkioTicks: 0, 60 | GuestTime: 0, 61 | CguestTime: 0, 62 | StartData: 6340112, 63 | EndData: 6341364, 64 | StartBrk: 24690688, 65 | ArgStart: 140724300791495, 66 | ArgEnd: 140724300791515, 67 | EnvStart: 140724300791515, 68 | EnvEnd: 140724300791791, 69 | ExitCode: 0, 70 | }) { 71 | t.Error(proc) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /procstats/linux/statm.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import "fmt" 4 | 5 | // ProcStatm contains statistics about memory utilization of a process. 6 | type ProcStatm struct { 7 | Size uint64 // (1) size 8 | Resident uint64 // (2) resident 9 | Share uint64 // (3) share 10 | Text uint64 // (4) text 11 | Lib uint64 // (5) lib 12 | Data uint64 // (6) data 13 | Dt uint64 // (7) dt 14 | } 15 | 16 | // ReadProcStatm returns a ProcStatm and an error, if any, for a PID. 17 | func ReadProcStatm(pid int) (proc ProcStatm, err error) { 18 | defer func() { err = convertPanicToError(recover()) }() 19 | return ParseProcStatm(readProcFile(pid, "statm")) 20 | } 21 | 22 | // ParseProcStatm parses system proc data and returns a ProcStatm and error, if any. 23 | func ParseProcStatm(s string) (proc ProcStatm, err error) { 24 | _, err = fmt.Sscan(s, 25 | &proc.Size, 26 | &proc.Resident, 27 | &proc.Share, 28 | &proc.Text, 29 | &proc.Lib, 30 | &proc.Data, 31 | &proc.Dt, 32 | ) 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /procstats/linux/statm_darwin_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadProcStatm(t *testing.T) { 9 | if _, err := ReadProcStatm(os.Getpid()); err == nil { 10 | t.Error("ReadProcStatm should have failed on Darwin") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /procstats/linux/statm_linux_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestReadProcStatm(t *testing.T) { 9 | if statm, err := ReadProcStatm(os.Getpid()); err != nil { 10 | t.Error("ReadProcStatm:", err) 11 | } else if statm.Size == 0 { 12 | t.Error("ReadPorcStatm: size cannot be zero") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /procstats/linux/statm_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseProcStatm(t *testing.T) { 9 | text := `1134 172 153 12 0 115 0` 10 | 11 | proc, err := ParseProcStatm(text) 12 | if err != nil { 13 | t.Error(err) 14 | return 15 | } 16 | 17 | if !reflect.DeepEqual(proc, ProcStatm{ 18 | Size: 1134, 19 | Resident: 172, 20 | Share: 153, 21 | Text: 12, 22 | Lib: 0, 23 | Data: 115, 24 | Dt: 0, 25 | }) { 26 | t.Error(proc) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /procstats/proc_test.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | "testing" 10 | "time" 11 | 12 | "github.com/segmentio/stats/v5" 13 | "github.com/segmentio/stats/v5/statstest" 14 | ) 15 | 16 | func TestProcMetrics(t *testing.T) { 17 | t.Run("self", func(t *testing.T) { 18 | testProcMetrics(t, os.Getpid()) 19 | }) 20 | t.Run("child", func(t *testing.T) { 21 | cmd := exec.Command("yes") 22 | cmd.Stdin = os.Stdin 23 | cmd.Stdout = io.Discard 24 | cmd.Stderr = io.Discard 25 | 26 | if err := cmd.Start(); err != nil { 27 | t.Fatal(err) 28 | } 29 | time.Sleep(200 * time.Millisecond) 30 | testProcMetrics(t, cmd.Process.Pid) 31 | cmd.Process.Signal(os.Interrupt) 32 | waitErr := cmd.Wait() 33 | //revive:disable-next-line 34 | if exitErr, ok := waitErr.(*exec.ExitError); ok && exitErr.Error() == "signal: interrupt" { 35 | // This is expected from stopping the process 36 | } else { 37 | t.Fatal(waitErr) 38 | } 39 | }) 40 | } 41 | 42 | func testProcMetrics(t *testing.T, pid int) { 43 | t.Helper() 44 | h := &statstest.Handler{} 45 | e := stats.NewEngine("", h) 46 | 47 | proc := NewProcMetricsWith(e, pid) 48 | 49 | // for darwin - catch the "can't collect child metrics" error before 50 | // starting the test 51 | _, err := CollectProcInfo(proc.pid) 52 | var o *OSUnsupportedError 53 | if errors.As(err, &o) { 54 | t.Skipf("can't run test because current OS is unsupported: %v", runtime.GOOS) 55 | } 56 | 57 | for i := 0; i != 10; i++ { 58 | t.Logf("collect number %d", i) 59 | proc.Collect() 60 | 61 | if len(h.Measures()) == 0 { 62 | t.Error("no measures were reported by the stats collector") 63 | } 64 | 65 | for _, m := range h.Measures() { 66 | t.Log(m) 67 | } 68 | 69 | h.Clear() 70 | time.Sleep(10 * time.Millisecond) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /procstats/proc_windows.go: -------------------------------------------------------------------------------- 1 | package procstats 2 | 3 | func collectProcMetrics(pid int) (m proc, err error) { 4 | // TODO 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /prometheus/append_test.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var testMetrics = []struct { 9 | scenario string 10 | metric metric 11 | string string 12 | }{ 13 | { 14 | scenario: "simple counter metric", 15 | metric: metric{ 16 | mtype: counter, 17 | name: "hello_world", 18 | value: 42, 19 | }, 20 | string: `# TYPE hello_world counter 21 | hello_world 42 22 | `, 23 | }, 24 | 25 | { 26 | scenario: "simple gauge metric", 27 | metric: metric{ 28 | mtype: gauge, 29 | name: "hello_world", 30 | value: 42, 31 | }, 32 | string: `# TYPE hello_world gauge 33 | hello_world 42 34 | `, 35 | }, 36 | 37 | { 38 | scenario: "simple histogram metric", 39 | metric: metric{ 40 | mtype: histogram, 41 | name: "hello_world_bucket", 42 | value: 42, 43 | labels: labels{{"le", "0.5"}}, 44 | }, 45 | string: `# TYPE hello_world histogram 46 | hello_world_bucket{le="0.5"} 42 47 | `, 48 | }, 49 | 50 | { 51 | scenario: "counter metric with help, floating point value, labels, and timestamp", 52 | metric: metric{ 53 | mtype: counter, 54 | scope: "global", 55 | name: "hello_world", 56 | help: "This is a great metric!\n", 57 | value: 0.5, 58 | labels: []label{{"question", "\"???\"\n"}, {"answer", "42"}}, 59 | time: time.Date(2017, 6, 4, 22, 12, 0, 0, time.UTC), 60 | }, 61 | string: `# HELP global_hello_world This is a great metric!\n 62 | # TYPE global_hello_world counter 63 | global_hello_world{question="\"???\"\n",answer="42"} 0.5 1496614320000 64 | `, 65 | }, 66 | } 67 | 68 | func TestAppendMetric(t *testing.T) { 69 | for _, test := range testMetrics { 70 | t.Run(test.scenario, func(t *testing.T) { 71 | b := appendMetric(nil, test.metric) 72 | s := string(b) 73 | 74 | if s != test.string { 75 | t.Error("bad metric representation:") 76 | t.Log(test.string) 77 | t.Log(s) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func BenchmarkAppendMetric(b *testing.B) { 84 | a := make([]byte, 8192) 85 | 86 | for _, test := range testMetrics { 87 | b.Run(test.scenario, func(b *testing.B) { 88 | for i := 0; i != b.N; i++ { 89 | appendMetric(a[:0], test.metric) 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /prometheus/label.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "github.com/segmentio/fasthash/jody" 5 | 6 | "github.com/segmentio/stats/v5" 7 | ) 8 | 9 | type label struct { 10 | name string 11 | value string 12 | } 13 | 14 | func (l label) equal(other label) bool { 15 | return l.name == other.name && l.value == other.value 16 | } 17 | 18 | func (l label) less(other label) bool { 19 | return l.name < other.name || (l.name == other.name && l.value < other.value) 20 | } 21 | 22 | type labels []label 23 | 24 | func makeLabels(l ...label) labels { 25 | m := make(labels, len(l)) 26 | copy(m, l) 27 | return m 28 | } 29 | 30 | func (l labels) copyAppend(m ...label) labels { 31 | c := make(labels, 0, len(l)+len(m)) 32 | c = append(c, l...) 33 | c = append(c, m...) 34 | return c 35 | } 36 | 37 | func (l labels) copy() labels { 38 | return makeLabels(l...) 39 | } 40 | 41 | func (l labels) hash() uint64 { 42 | h := jody.Init64 43 | 44 | for i := range l { 45 | h = jody.AddString64(h, l[i].name) 46 | h = jody.AddString64(h, l[i].value) 47 | } 48 | 49 | return h 50 | } 51 | 52 | func (l labels) equal(other labels) bool { 53 | if len(l) != len(other) { 54 | return false 55 | } 56 | for i := range l { 57 | if !l[i].equal(other[i]) { 58 | return false 59 | } 60 | } 61 | return true 62 | } 63 | 64 | func (l labels) less(other labels) bool { 65 | n1 := len(l) 66 | n2 := len(other) 67 | 68 | for i := 0; i != n1 && i != n2; i++ { 69 | if !l[i].equal(other[i]) { 70 | return l[i].less(other[i]) 71 | } 72 | } 73 | 74 | return n1 < n2 75 | } 76 | 77 | func (l labels) appendTags(tags ...stats.Tag) labels { 78 | for _, t := range tags { 79 | l = append(l, label{name: t.Name, value: t.Value}) 80 | } 81 | return l 82 | } 83 | -------------------------------------------------------------------------------- /prometheus/label_test.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import "testing" 4 | 5 | func TestLabelsLess(t *testing.T) { 6 | tests := []struct { 7 | l1 labels 8 | l2 labels 9 | less bool 10 | }{ 11 | { 12 | l1: labels{}, 13 | l2: labels{}, 14 | less: false, 15 | }, 16 | 17 | { 18 | l1: labels{}, 19 | l2: labels{{"id", "123"}}, 20 | less: true, 21 | }, 22 | 23 | { 24 | l1: labels{{"id", "123"}}, 25 | l2: labels{}, 26 | less: false, 27 | }, 28 | 29 | { 30 | l1: labels{{"id", "123"}}, 31 | l2: labels{{"id", "123"}}, 32 | less: false, 33 | }, 34 | 35 | { 36 | l1: labels{{"a", "1"}}, 37 | l2: labels{{"a", "1"}, {"b", "2"}}, 38 | less: true, 39 | }, 40 | 41 | { 42 | l1: labels{{"a", "1"}, {"b", "2"}}, 43 | l2: labels{{"a", "1"}}, 44 | less: false, 45 | }, 46 | 47 | { 48 | l1: labels{{"a", "1"}, {"b", "2"}}, 49 | l2: labels{{"a", "1"}, {"b", "2"}}, 50 | less: false, 51 | }, 52 | } 53 | 54 | for _, test := range tests { 55 | t.Run("", func(t *testing.T) { 56 | if less := test.l1.less(test.l2); less != test.less { 57 | t.Errorf("(%#v < %#v) != %t", test.l1, test.l2, test.less) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /reflect.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "reflect" 5 | "time" 6 | "unsafe" 7 | ) 8 | 9 | type structField struct { 10 | typ reflect.Type 11 | off uintptr 12 | } 13 | 14 | func (f structField) pointer(ptr unsafe.Pointer) unsafe.Pointer { 15 | return unsafe.Pointer(uintptr(ptr) + f.off) 16 | } 17 | 18 | func (f structField) bool(ptr unsafe.Pointer) bool { 19 | return *(*bool)(f.pointer(ptr)) 20 | } 21 | 22 | func (f structField) int(ptr unsafe.Pointer) int { 23 | return *(*int)(f.pointer(ptr)) 24 | } 25 | 26 | func (f structField) int8(ptr unsafe.Pointer) int8 { 27 | return *(*int8)(f.pointer(ptr)) 28 | } 29 | 30 | func (f structField) int16(ptr unsafe.Pointer) int16 { 31 | return *(*int16)(f.pointer(ptr)) 32 | } 33 | 34 | func (f structField) int32(ptr unsafe.Pointer) int32 { 35 | return *(*int32)(f.pointer(ptr)) 36 | } 37 | 38 | func (f structField) int64(ptr unsafe.Pointer) int64 { 39 | return *(*int64)(f.pointer(ptr)) 40 | } 41 | 42 | func (f structField) uint(ptr unsafe.Pointer) uint { 43 | return *(*uint)(f.pointer(ptr)) 44 | } 45 | 46 | func (f structField) uint8(ptr unsafe.Pointer) uint8 { 47 | return *(*uint8)(f.pointer(ptr)) 48 | } 49 | 50 | func (f structField) uint16(ptr unsafe.Pointer) uint16 { 51 | return *(*uint16)(f.pointer(ptr)) 52 | } 53 | 54 | func (f structField) uint32(ptr unsafe.Pointer) uint32 { 55 | return *(*uint32)(f.pointer(ptr)) 56 | } 57 | 58 | func (f structField) uint64(ptr unsafe.Pointer) uint64 { 59 | return *(*uint64)(f.pointer(ptr)) 60 | } 61 | 62 | func (f structField) uintptr(ptr unsafe.Pointer) uintptr { 63 | return *(*uintptr)(f.pointer(ptr)) 64 | } 65 | 66 | func (f structField) float32(ptr unsafe.Pointer) float32 { 67 | return *(*float32)(f.pointer(ptr)) 68 | } 69 | 70 | func (f structField) float64(ptr unsafe.Pointer) float64 { 71 | return *(*float64)(f.pointer(ptr)) 72 | } 73 | 74 | func (f structField) duration(ptr unsafe.Pointer) time.Duration { 75 | return *(*time.Duration)(f.pointer(ptr)) 76 | } 77 | 78 | func (f structField) string(ptr unsafe.Pointer) string { 79 | return *(*string)(f.pointer(ptr)) 80 | } 81 | 82 | var ( 83 | boolType = reflect.TypeOf(false) 84 | intType = reflect.TypeOf(int(0)) 85 | int8Type = reflect.TypeOf(int8(0)) 86 | int16Type = reflect.TypeOf(int16(0)) 87 | int32Type = reflect.TypeOf(int32(0)) 88 | int64Type = reflect.TypeOf(int64(0)) 89 | uintType = reflect.TypeOf(uint(0)) 90 | uint8Type = reflect.TypeOf(uint8(0)) 91 | uint16Type = reflect.TypeOf(uint16(0)) 92 | uint32Type = reflect.TypeOf(uint32(0)) 93 | uint64Type = reflect.TypeOf(uint64(0)) 94 | uintptrType = reflect.TypeOf(uintptr(0)) 95 | float32Type = reflect.TypeOf(float32(0)) 96 | float64Type = reflect.TypeOf(float64(0)) 97 | durationType = reflect.TypeOf(time.Duration(0)) 98 | stringType = reflect.TypeOf("") 99 | ) 100 | -------------------------------------------------------------------------------- /statstest/handler.go: -------------------------------------------------------------------------------- 1 | package statstest 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "time" 7 | 8 | stats "github.com/segmentio/stats/v5" 9 | ) 10 | 11 | var ( 12 | _ stats.Handler = (*Handler)(nil) 13 | _ stats.Flusher = (*Handler)(nil) 14 | ) 15 | 16 | // Handler is a stats handler that can record measures for inspection. 17 | type Handler struct { 18 | sync.Mutex 19 | measures []stats.Measure 20 | flush int32 21 | } 22 | 23 | // HandleMeasures process a variadic list of stats.Measure. 24 | func (h *Handler) HandleMeasures(_ time.Time, measures ...stats.Measure) { 25 | h.Lock() 26 | for _, m := range measures { 27 | h.measures = append(h.measures, m.Clone()) 28 | } 29 | h.Unlock() 30 | } 31 | 32 | // Measures returns a copy of the handled measures. 33 | func (h *Handler) Measures() []stats.Measure { 34 | h.Lock() 35 | m := make([]stats.Measure, len(h.measures)) 36 | copy(m, h.measures) 37 | h.Unlock() 38 | return m 39 | } 40 | 41 | // Flush Increments Flush counter. 42 | func (h *Handler) Flush() { 43 | atomic.AddInt32(&h.flush, 1) 44 | } 45 | 46 | // FlushCalls returns the number of times `Flush` has been invoked. 47 | func (h *Handler) FlushCalls() int { 48 | return int(atomic.LoadInt32(&h.flush)) 49 | } 50 | 51 | // Clear removes all measures held by Handler. 52 | func (h *Handler) Clear() { 53 | h.Lock() 54 | h.measures = h.measures[:0] 55 | h.Unlock() 56 | } 57 | -------------------------------------------------------------------------------- /util/objconv/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Segment 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /util/objconv/adapter.go: -------------------------------------------------------------------------------- 1 | package objconv 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | ) 7 | 8 | // An Adapter is a pair of an encoder and a decoder function that can be 9 | // installed on the package to support new types. 10 | type Adapter struct { 11 | Encode func(Encoder, reflect.Value) error 12 | Decode func(Decoder, reflect.Value) error 13 | } 14 | 15 | // Install adds an adapter for typ. 16 | // 17 | // The function panics if one of the encoder and decoder functions of the 18 | // adapter are nil. 19 | // 20 | // A typical use case for this function is to be called during the package 21 | // initialization phase to extend objconv support for new types. 22 | func Install(typ reflect.Type, adapter Adapter) { 23 | if adapter.Encode == nil { 24 | panic("objconv: the encoder function of an adapter cannot be nil") 25 | } 26 | 27 | if adapter.Decode == nil { 28 | panic("objconv: the decoder function of an adapter cannot be nil") 29 | } 30 | 31 | adapterMutex.Lock() 32 | adapterStore[typ] = adapter 33 | adapterMutex.Unlock() 34 | 35 | // We have to clear the struct cache because it may now have become invalid. 36 | // Because installing adapters is done in the package initialization phase 37 | // it's unlikely that any encoding or decoding operations are taking place 38 | // at this time so there should be no performance impact of clearing the 39 | // cache. 40 | structCache.clear() 41 | } 42 | 43 | // AdapterOf returns the adapter for typ, setting ok to true if one was found, 44 | // false otherwise. 45 | func AdapterOf(typ reflect.Type) (a Adapter, ok bool) { 46 | adapterMutex.RLock() 47 | a, ok = adapterStore[typ] 48 | adapterMutex.RUnlock() 49 | return 50 | } 51 | 52 | var ( 53 | adapterMutex sync.RWMutex 54 | adapterStore = make(map[reflect.Type]Adapter) 55 | ) 56 | -------------------------------------------------------------------------------- /util/objconv/codec.go: -------------------------------------------------------------------------------- 1 | package objconv 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | // A Codec is a factory for encoder and decoders that work on byte streams. 9 | type Codec struct { 10 | NewEmitter func(io.Writer) Emitter 11 | NewParser func(io.Reader) Parser 12 | } 13 | 14 | // NewEncoder returns a new encoder that outputs to w. 15 | func (c Codec) NewEncoder(w io.Writer) *Encoder { 16 | return NewEncoder(c.NewEmitter(w)) 17 | } 18 | 19 | // NewDecoder returns a new decoder that takes input from r. 20 | func (c Codec) NewDecoder(r io.Reader) *Decoder { 21 | return NewDecoder(c.NewParser(r)) 22 | } 23 | 24 | // NewStreamEncoder returns a new stream encoder that outputs to w. 25 | func (c Codec) NewStreamEncoder(w io.Writer) *StreamEncoder { 26 | return NewStreamEncoder(c.NewEmitter(w)) 27 | } 28 | 29 | // NewStreamDecoder returns a new stream decoder that takes input from r. 30 | func (c Codec) NewStreamDecoder(r io.Reader) *StreamDecoder { 31 | return NewStreamDecoder(c.NewParser(r)) 32 | } 33 | 34 | // A Registry associates mime types to codecs. 35 | // 36 | // It is safe to use a registry concurrently from multiple goroutines. 37 | type Registry struct { 38 | mutex sync.RWMutex 39 | codecs map[string]Codec 40 | } 41 | 42 | // Register adds a codec for a mimetype to r. 43 | func (reg *Registry) Register(mimetype string, codec Codec) { 44 | defer reg.mutex.Unlock() 45 | reg.mutex.Lock() 46 | 47 | if reg.codecs == nil { 48 | reg.codecs = make(map[string]Codec) 49 | } 50 | 51 | reg.codecs[mimetype] = codec 52 | } 53 | 54 | // Unregister removes the codec for a mimetype from r. 55 | func (reg *Registry) Unregister(mimetype string) { 56 | defer reg.mutex.Unlock() 57 | reg.mutex.Lock() 58 | 59 | delete(reg.codecs, mimetype) 60 | } 61 | 62 | // Lookup returns the codec associated with mimetype, ok is set to true or false 63 | // based on whether a codec was found. 64 | func (reg *Registry) Lookup(mimetype string) (codec Codec, ok bool) { 65 | reg.mutex.RLock() 66 | codec, ok = reg.codecs[mimetype] 67 | reg.mutex.RUnlock() 68 | return 69 | } 70 | 71 | // Codecs returns a map of all codecs registered in reg. 72 | func (reg *Registry) Codecs() (codecs map[string]Codec) { 73 | codecs = make(map[string]Codec) 74 | reg.mutex.RLock() 75 | for mimetype, codec := range reg.codecs { 76 | codecs[mimetype] = codec 77 | } 78 | reg.mutex.RUnlock() 79 | return 80 | } 81 | 82 | // The global registry to which packages add their codecs. 83 | var registry Registry 84 | 85 | // Register adds a codec for a mimetype to the global registry. 86 | func Register(mimetype string, codec Codec) { 87 | registry.Register(mimetype, codec) 88 | } 89 | 90 | // Unregister removes the codec for a mimetype from the global registry. 91 | func Unregister(mimetype string) { 92 | registry.Unregister(mimetype) 93 | } 94 | 95 | // Lookup returns the codec associated with mimetype, ok is set to true or false 96 | // based on whether a codec was found. 97 | func Lookup(mimetype string) (Codec, bool) { 98 | return registry.Lookup(mimetype) 99 | } 100 | 101 | // Codecs returns a map of all codecs registered in the global registry. 102 | func Codecs() map[string]Codec { 103 | return registry.Codecs() 104 | } 105 | -------------------------------------------------------------------------------- /util/objconv/error.go: -------------------------------------------------------------------------------- 1 | package objconv 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func typeConversionError(from, to Type) error { 9 | return fmt.Errorf("objconv: cannot convert from %s to %s", from, to) 10 | } 11 | 12 | // End is expected to be returned to indicate that a function has completed 13 | // its work, this is usually employed in generic algorithms. 14 | // 15 | //revive:disable:error-naming 16 | var End = errors.New("end") //nolint:staticcheck 17 | -------------------------------------------------------------------------------- /util/objconv/json/decode.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "sync" 7 | 8 | "github.com/segmentio/stats/v5/util/objconv" 9 | ) 10 | 11 | // NewDecoder returns a new JSON decoder that parses values from r. 12 | func NewDecoder(r io.Reader) *objconv.Decoder { 13 | return objconv.NewDecoder(NewParser(r)) 14 | } 15 | 16 | // NewStreamDecoder returns a new JSON stream decoder that parses values from r. 17 | func NewStreamDecoder(r io.Reader) *objconv.StreamDecoder { 18 | return objconv.NewStreamDecoder(NewParser(r)) 19 | } 20 | 21 | // Unmarshal decodes a JSON representation of v from b. 22 | func Unmarshal(b []byte, v interface{}) error { 23 | u := unmarshalerPool.Get().(*unmarshaler) 24 | u.reset(b) 25 | 26 | err := (objconv.Decoder{Parser: u}).Decode(v) 27 | 28 | u.reset(nil) 29 | unmarshalerPool.Put(u) 30 | return err 31 | } 32 | 33 | var unmarshalerPool = sync.Pool{ 34 | New: func() interface{} { return newUnmarshaler() }, 35 | } 36 | 37 | type unmarshaler struct { 38 | Parser 39 | b bytes.Buffer 40 | } 41 | 42 | func newUnmarshaler() *unmarshaler { 43 | u := &unmarshaler{} 44 | u.s = u.c[:0] 45 | u.r = &u.b 46 | return u 47 | } 48 | 49 | func (u *unmarshaler) reset(b []byte) { 50 | u.b = *bytes.NewBuffer(b) 51 | u.Reset(&u.b) 52 | } 53 | -------------------------------------------------------------------------------- /util/objconv/json/encode.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "sync" 7 | 8 | "github.com/segmentio/stats/v5/util/objconv" 9 | ) 10 | 11 | // NewEncoder returns a new JSON encoder that writes to w. 12 | func NewEncoder(w io.Writer) *objconv.Encoder { 13 | return objconv.NewEncoder(NewEmitter(w)) 14 | } 15 | 16 | // NewStreamEncoder returns a new JSON stream encoder that writes to w. 17 | func NewStreamEncoder(w io.Writer) *objconv.StreamEncoder { 18 | return objconv.NewStreamEncoder(NewEmitter(w)) 19 | } 20 | 21 | // NewPrettyEncoder returns a new JSON encoder that writes to w. 22 | func NewPrettyEncoder(w io.Writer) *objconv.Encoder { 23 | return objconv.NewEncoder(NewPrettyEmitter(w)) 24 | } 25 | 26 | // NewPrettyStreamEncoder returns a new JSON stream encoder that writes to w. 27 | func NewPrettyStreamEncoder(w io.Writer) *objconv.StreamEncoder { 28 | return objconv.NewStreamEncoder(NewPrettyEmitter(w)) 29 | } 30 | 31 | // Marshal writes the JSON representation of v to a byte slice returned in b. 32 | func Marshal(v interface{}) (b []byte, err error) { 33 | m := marshalerPool.Get().(*marshaler) 34 | m.b.Truncate(0) 35 | 36 | if err = (objconv.Encoder{Emitter: m}).Encode(v); err == nil { 37 | b = make([]byte, m.b.Len()) 38 | copy(b, m.b.Bytes()) 39 | } 40 | 41 | marshalerPool.Put(m) 42 | return 43 | } 44 | 45 | var marshalerPool = sync.Pool{ 46 | New: func() interface{} { return newMarshaler() }, 47 | } 48 | 49 | type marshaler struct { 50 | Emitter 51 | b bytes.Buffer 52 | } 53 | 54 | func newMarshaler() *marshaler { 55 | m := &marshaler{} 56 | m.s = m.a[:0] 57 | m.w = &m.b 58 | return m 59 | } 60 | -------------------------------------------------------------------------------- /util/objconv/json/init.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/segmentio/stats/v5/util/objconv" 7 | ) 8 | 9 | // Codec for the JSON format. 10 | var Codec = objconv.Codec{ 11 | NewEmitter: func(w io.Writer) objconv.Emitter { return NewEmitter(w) }, 12 | NewParser: func(r io.Reader) objconv.Parser { return NewParser(r) }, 13 | } 14 | 15 | // PrettyCodec for the JSON format. 16 | var PrettyCodec = objconv.Codec{ 17 | NewEmitter: func(w io.Writer) objconv.Emitter { return NewPrettyEmitter(w) }, 18 | NewParser: func(r io.Reader) objconv.Parser { return NewParser(r) }, 19 | } 20 | 21 | func init() { 22 | for _, name := range [...]string{ 23 | "application/json", 24 | "text/json", 25 | "json", 26 | } { 27 | objconv.Register(name, Codec) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /util/objconv/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/segmentio/stats/v5/util/objconv/objtests" 10 | ) 11 | 12 | func TestCodec(t *testing.T) { 13 | objtests.TestCodec(t, Codec) 14 | } 15 | 16 | func BenchmarkCodec(b *testing.B) { 17 | objtests.BenchmarkCodec(b, Codec) 18 | } 19 | 20 | func TestPrettyCodec(t *testing.T) { 21 | objtests.TestCodec(t, PrettyCodec) 22 | } 23 | 24 | func BenchmarkPrettyCodec(b *testing.B) { 25 | objtests.BenchmarkCodec(b, PrettyCodec) 26 | } 27 | 28 | func TestUnicode(t *testing.T) { 29 | tests := []struct { 30 | in string 31 | out string 32 | }{ 33 | {`"\u2022"`, "•"}, 34 | {`"\uDC00D800"`, "�"}, 35 | } 36 | 37 | for _, test := range tests { 38 | t.Run(test.out, func(t *testing.T) { 39 | var s string 40 | 41 | if err := Unmarshal([]byte(test.in), &s); err != nil { 42 | t.Error(err) 43 | } 44 | 45 | if s != test.out { 46 | t.Error(s) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestMapValueOverflow(t *testing.T) { 53 | src := fmt.Sprintf( 54 | `{"A":"good","skip1":"%s","B":"bad","skip2":"%sA"}`, 55 | strings.Repeat("0", 102), 56 | strings.Repeat("0", 110), 57 | ) 58 | 59 | val := struct{ A string }{} 60 | 61 | if err := Unmarshal([]byte(src), &val); err != nil { 62 | t.Error(err) 63 | return 64 | } 65 | 66 | if val.A != "good" { 67 | t.Error(val.A) 68 | } 69 | } 70 | 71 | func TestEmitImpossibleFloats(t *testing.T) { 72 | values := []float64{ 73 | math.NaN(), 74 | math.Inf(+1), 75 | math.Inf(-1), 76 | } 77 | 78 | for _, v := range values { 79 | t.Run(fmt.Sprintf("emitting %v must return an error", v), func(t *testing.T) { 80 | e := Emitter{} 81 | 82 | if err := e.EmitFloat(v, 64); err == nil { 83 | t.Error("no error was returned") 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /util/objconv/objutil/duration.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | // Copyright 2009 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | import "time" 8 | 9 | // AppendDuration appends a human-readable representation of d to b. 10 | // 11 | // The function copies the implementation of time.Duration.String but prevents 12 | // Go from making a dynamic memory allocation on the returned value. 13 | func AppendDuration(b []byte, d time.Duration) []byte { 14 | // Largest time is 2540400h10m10.000000000s 15 | var buf [32]byte 16 | w := len(buf) 17 | 18 | u := uint64(d) 19 | neg := d < 0 20 | if neg { 21 | u = -u 22 | } 23 | 24 | if u < uint64(time.Second) { 25 | // Special case: if duration is smaller than a second, 26 | // use smaller units, like 1.2ms 27 | var prec int 28 | w-- 29 | buf[w] = 's' 30 | w-- 31 | switch { 32 | case u == 0: 33 | return append(b, '0', 's') 34 | case u < uint64(time.Microsecond): 35 | // print nanoseconds 36 | prec = 0 37 | buf[w] = 'n' 38 | case u < uint64(time.Millisecond): 39 | // print microseconds 40 | prec = 3 41 | // U+00B5 'µ' micro sign == 0xC2 0xB5 42 | w-- // Need room for two bytes. 43 | copy(buf[w:], "µ") 44 | default: 45 | // print milliseconds 46 | prec = 6 47 | buf[w] = 'm' 48 | } 49 | w, u = fmtFrac(buf[:w], u, prec) 50 | w = fmtInt(buf[:w], u) 51 | } else { 52 | w-- 53 | buf[w] = 's' 54 | 55 | w, u = fmtFrac(buf[:w], u, 9) 56 | 57 | // u is now integer seconds 58 | w = fmtInt(buf[:w], u%60) 59 | u /= 60 60 | 61 | // u is now integer minutes 62 | if u > 0 { 63 | w-- 64 | buf[w] = 'm' 65 | w = fmtInt(buf[:w], u%60) 66 | u /= 60 67 | 68 | // u is now integer hours 69 | // Stop at hours because days can be different lengths. 70 | if u > 0 { 71 | w-- 72 | buf[w] = 'h' 73 | w = fmtInt(buf[:w], u) 74 | } 75 | } 76 | } 77 | 78 | if neg { 79 | w-- 80 | buf[w] = '-' 81 | } 82 | 83 | return append(b, buf[w:]...) 84 | } 85 | 86 | // fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the 87 | // tail of buf, omitting trailing zeros. it omits the decimal 88 | // point too when the fraction is 0. It returns the index where the 89 | // output bytes begin and the value v/10**prec. 90 | func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) { 91 | // Omit trailing zeros up to and including decimal point. 92 | w := len(buf) 93 | shouldPrint := false 94 | for i := 0; i < prec; i++ { 95 | digit := v % 10 96 | shouldPrint = shouldPrint || digit != 0 97 | if shouldPrint { 98 | w-- 99 | buf[w] = byte(digit) + '0' 100 | } 101 | v /= 10 102 | } 103 | if shouldPrint { 104 | w-- 105 | buf[w] = '.' 106 | } 107 | return w, v 108 | } 109 | 110 | // fmtInt formats v into the tail of buf. 111 | // It returns the index where the output begins. 112 | func fmtInt(buf []byte, v uint64) int { 113 | w := len(buf) 114 | if v == 0 { 115 | w-- 116 | buf[w] = '0' 117 | } else { 118 | for v > 0 { 119 | w-- 120 | buf[w] = byte(v%10) + '0' 121 | v /= 10 122 | } 123 | } 124 | return w 125 | } 126 | -------------------------------------------------------------------------------- /util/objconv/objutil/duration_test.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var durationTests = []time.Duration{ 9 | 0, 10 | time.Nanosecond, 11 | time.Microsecond, 12 | time.Millisecond, 13 | time.Second, 14 | time.Minute, 15 | time.Hour, 16 | time.Hour + time.Minute + time.Second + (123456789 * time.Nanosecond), 17 | } 18 | 19 | func TestAppendDuration(t *testing.T) { 20 | for _, test := range durationTests { 21 | t.Run(test.String(), func(t *testing.T) { 22 | b := AppendDuration(nil, test) 23 | s := string(b) 24 | 25 | if s != test.String() { 26 | t.Error(s) 27 | } 28 | }) 29 | } 30 | } 31 | 32 | func BenchmarkAppendDuration(b *testing.B) { 33 | for _, test := range durationTests { 34 | b.Run(test.String(), func(b *testing.B) { 35 | var a [32]byte 36 | for i := 0; i != b.N; i++ { 37 | AppendDuration(a[:0], test) 38 | } 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /util/objconv/objutil/empty.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | // Copyright 2009 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | import ( 8 | "reflect" 9 | "unsafe" 10 | ) 11 | 12 | // IsEmpty returns true if the value given as argument would be considered 13 | // empty by the standard library packages, and therefore not serialized if 14 | // `omitempty` is set on a struct field with this value. 15 | func IsEmpty(v interface{}) bool { 16 | return IsEmptyValue(reflect.ValueOf(v)) 17 | } 18 | 19 | // IsEmptyValue returns true if the value given as argument would be considered 20 | // empty by the standard library packages, and therefore not serialized if 21 | // `omitempty` is set on a struct field with this value. 22 | // 23 | // Based on https://golang.org/src/encoding/json/encode.go?h=isEmpty 24 | func IsEmptyValue(v reflect.Value) bool { 25 | if !v.IsValid() { 26 | return true // nil interface{} 27 | } 28 | switch v.Kind() { 29 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 30 | return v.Len() == 0 31 | case reflect.Bool: 32 | return !v.Bool() 33 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 34 | return v.Int() == 0 35 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 36 | return v.Uint() == 0 37 | case reflect.Float32, reflect.Float64: 38 | return v.Float() == 0 39 | case reflect.Interface, reflect.Ptr, reflect.Chan, reflect.Func: 40 | return v.IsNil() 41 | case reflect.UnsafePointer: 42 | return unsafe.Pointer(v.Pointer()) == nil 43 | } 44 | return false 45 | } 46 | -------------------------------------------------------------------------------- /util/objconv/objutil/empty_test.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "unsafe" 7 | ) 8 | 9 | func TestIsEmptyTrue(t *testing.T) { 10 | tests := []interface{}{ 11 | nil, 12 | 13 | false, 14 | 15 | int(0), 16 | int8(0), 17 | int16(0), 18 | int32(0), 19 | int64(0), 20 | 21 | uint(0), 22 | uint8(0), 23 | uint16(0), 24 | uint32(0), 25 | uint64(0), 26 | uintptr(0), 27 | 28 | float32(0), 29 | float64(0), 30 | 31 | "", 32 | []byte(nil), 33 | []int{}, 34 | [...]int{}, 35 | 36 | (map[string]int)(nil), 37 | map[string]int{}, 38 | 39 | (*int)(nil), 40 | unsafe.Pointer(nil), 41 | 42 | (chan struct{})(nil), 43 | (func())(nil), 44 | } 45 | 46 | for _, test := range tests { 47 | t.Run(fmt.Sprintf("%T", test), func(t *testing.T) { 48 | if !IsEmpty(test) { 49 | t.Errorf("%T, %#v should be an empty value", test, test) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | func TestIsEmptyFalse(t *testing.T) { 56 | answer := 42 57 | 58 | tests := []interface{}{ 59 | true, 60 | 61 | int(1), 62 | int8(1), 63 | int16(1), 64 | int32(1), 65 | int64(1), 66 | 67 | uint(1), 68 | uint8(1), 69 | uint16(1), 70 | uint32(1), 71 | uint64(1), 72 | uintptr(1), 73 | 74 | float32(1), 75 | float64(1), 76 | 77 | "Hello World!", 78 | []byte("Hello World!"), 79 | []int{1, 2, 3}, 80 | [...]int{1, 2, 3}, 81 | 82 | map[string]int{"answer": 42}, 83 | 84 | &answer, 85 | unsafe.Pointer(&answer), 86 | 87 | make(chan struct{}), 88 | func() {}, 89 | } 90 | 91 | for _, test := range tests { 92 | t.Run(fmt.Sprintf("%T", test), func(t *testing.T) { 93 | if IsEmpty(test) { 94 | t.Errorf("%T, %#v should not be an empty value", test, test) 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /util/objconv/objutil/int_test.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | import "testing" 4 | 5 | var parseIntTests = []struct { 6 | v int64 7 | s string 8 | }{ 9 | {0, "0"}, 10 | {1, "1"}, 11 | {-1, "-1"}, 12 | {1234567890, "1234567890"}, 13 | {-1234567890, "-1234567890"}, 14 | {9223372036854775807, "9223372036854775807"}, 15 | {-9223372036854775808, "-9223372036854775808"}, 16 | } 17 | 18 | func TestParseInt(t *testing.T) { 19 | for _, test := range parseIntTests { 20 | t.Run(test.s, func(t *testing.T) { 21 | v, err := ParseInt([]byte(test.s)) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | if v != test.v { 27 | t.Error(v) 28 | } 29 | }) 30 | } 31 | } 32 | 33 | func BenchmarkParseInt(b *testing.B) { 34 | for _, test := range parseIntTests { 35 | b.Run(test.s, func(b *testing.B) { 36 | s := []byte(test.s) 37 | 38 | for i := 0; i != b.N; i++ { 39 | ParseInt(s) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | var parseUintHexTests = []struct { 46 | v uint64 47 | s string 48 | }{ 49 | {0x0, "0"}, 50 | {0x1, "1"}, 51 | {0xA, "a"}, 52 | {0xA, "A"}, 53 | {0x10, "10"}, 54 | {0xABCDEF, "abcdef"}, 55 | {0xABCDEF, "ABCDEF"}, 56 | {0xFFFFFFFFFFFFFFFF, "FFFFFFFFFFFFFFFF"}, 57 | } 58 | 59 | func TestParseUintHex(t *testing.T) { 60 | for _, test := range parseUintHexTests { 61 | t.Run(test.s, func(t *testing.T) { 62 | v, err := ParseUintHex([]byte(test.s)) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | 67 | if v != test.v { 68 | t.Error(v) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func BenchmarkParseUintHex(b *testing.B) { 75 | for _, test := range parseUintHexTests { 76 | b.Run(test.s, func(b *testing.B) { 77 | s := []byte(test.s) 78 | 79 | for i := 0; i != b.N; i++ { 80 | ParseUintHex(s) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /util/objconv/objutil/limits.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | const ( 9 | // UintMax is the maximum value of a uint. 10 | UintMax = ^uint(0) 11 | 12 | // UintMin is the minimum value of a uint. 13 | UintMin = 0 14 | 15 | // Uint8Max is the maximum value of a uint8. 16 | Uint8Max = 255 17 | 18 | // Uint8Min is the minimum value of a uint8. 19 | Uint8Min = 0 20 | 21 | // Uint16Max is the maximum value of a uint16. 22 | Uint16Max = 65535 23 | 24 | // Uint16Min is the minimum value of a uint16. 25 | Uint16Min = 0 26 | 27 | // Uint32Max is the maximum value of a uint32. 28 | Uint32Max = 4294967295 29 | 30 | // Uint32Min is the minimum value of a uint32. 31 | Uint32Min = 0 32 | 33 | // Uint64Max is the maximum value of a uint64. 34 | Uint64Max = 18446744073709551615 35 | 36 | // Uint64Min is the minimum value of a uint64. 37 | Uint64Min = 0 38 | 39 | // UintptrMax is the maximum value of a uintptr. 40 | UintptrMax = ^uintptr(0) 41 | 42 | // UintptrMin is the minimum value of a uintptr. 43 | UintptrMin = 0 44 | 45 | // IntMax is the maximum value of a int. 46 | IntMax = int(UintMax >> 1) 47 | 48 | // IntMin is the minimum value of a int. 49 | IntMin = -IntMax - 1 50 | 51 | // Int8Max is the maximum value of a int8. 52 | Int8Max = 127 53 | 54 | // Int8Min is the minimum value of a int8. 55 | Int8Min = -128 56 | 57 | // Int16Max is the maximum value of a int16. 58 | Int16Max = 32767 59 | 60 | // Int16Min is the minimum value of a int16. 61 | Int16Min = -32768 62 | 63 | // Int32Max is the maximum value of a int32. 64 | Int32Max = 2147483647 65 | 66 | // Int32Min is the minimum value of a int32. 67 | Int32Min = -2147483648 68 | 69 | // Int64Max is the maximum value of a int64. 70 | Int64Max = 9223372036854775807 71 | 72 | // Int64Min is the minimum value of a int64. 73 | Int64Min = -9223372036854775808 74 | 75 | // Float32IntMax is the maximum consecutive integer value representable by a float32. 76 | Float32IntMax = 16777216 77 | 78 | // Float32IntMin is the minimum consecutive integer value representable by a float32. 79 | Float32IntMin = -16777216 80 | 81 | // Float64IntMax is the maximum consecutive integer value representable by a float64. 82 | Float64IntMax = 9007199254740992 83 | 84 | // Float64IntMin is the minimum consecutive integer value representable by a float64. 85 | Float64IntMin = -9007199254740992 86 | ) 87 | 88 | // CheckUint64Bounds verifies that v is smaller than max, t represents the 89 | // original type of v. 90 | func CheckUint64Bounds(v, maxVal uint64, t reflect.Type) error { 91 | if v > maxVal { 92 | return fmt.Errorf("objconv: %d overflows the maximum value of %d for %s", v, maxVal, t) 93 | } 94 | return nil 95 | } 96 | 97 | // CheckInt64Bounds verifies that v is within min and max, t represents the 98 | // original type of v. 99 | func CheckInt64Bounds(v, minVal int64, maxVal uint64, t reflect.Type) error { 100 | if v < minVal { 101 | return fmt.Errorf("objconv: %d overflows the minimum value of %d for %s", v, minVal, t) 102 | } 103 | if v > 0 && uint64(v) > maxVal { 104 | return fmt.Errorf("objconv: %d overflows the maximum value of %d for %s", v, maxVal, t) 105 | } 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /util/objconv/objutil/limits_test.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | -------------------------------------------------------------------------------- /util/objconv/objutil/tag.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | import "strings" 4 | 5 | // Tag represents the result of parsing the tag of a struct field. 6 | type Tag struct { 7 | // Name is the field name that should be used when serializing. 8 | Name string 9 | 10 | // Omitempty is true if the tag had `omitempty` set. 11 | Omitempty bool 12 | 13 | // Omitzero is true if the tag had `omitzero` set. 14 | Omitzero bool 15 | } 16 | 17 | // ParseTag parses a raw tag obtained from a struct field, returning the results 18 | // as a tag value. 19 | func ParseTag(s string) Tag { 20 | var name string 21 | var omitzero bool 22 | var omitempty bool 23 | 24 | name, s = parseNextTagToken(s) 25 | 26 | for len(s) != 0 { 27 | var token string 28 | switch token, s = parseNextTagToken(s); token { 29 | case "omitempty": 30 | omitempty = true 31 | case "omitzero": 32 | omitzero = true 33 | } 34 | } 35 | 36 | return Tag{ 37 | Name: name, 38 | Omitempty: omitempty, 39 | Omitzero: omitzero, 40 | } 41 | } 42 | 43 | // ParseTagJSON is similar to ParseTag but only supports features supported by 44 | // the standard encoding/json package. 45 | func ParseTagJSON(s string) Tag { 46 | var name string 47 | var omitempty bool 48 | 49 | name, s = parseNextTagToken(s) 50 | 51 | for len(s) != 0 { 52 | var token string 53 | switch token, s = parseNextTagToken(s); token { 54 | case "omitempty": 55 | omitempty = true 56 | } 57 | } 58 | 59 | return Tag{ 60 | Name: name, 61 | Omitempty: omitempty, 62 | } 63 | } 64 | 65 | func parseNextTagToken(s string) (string, string) { 66 | split := strings.IndexByte(s, ',') 67 | if split < 0 { 68 | token := s 69 | return token, "" 70 | } 71 | token, next := s[:split], s[split+1:] 72 | return token, next 73 | } 74 | -------------------------------------------------------------------------------- /util/objconv/objutil/tag_test.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | import "testing" 4 | 5 | func TestParseTag(t *testing.T) { 6 | tests := []struct { 7 | tag string 8 | res Tag 9 | }{ 10 | { 11 | tag: "", 12 | res: Tag{}, 13 | }, 14 | { 15 | tag: "hello", 16 | res: Tag{Name: "hello"}, 17 | }, 18 | { 19 | tag: ",omitempty", 20 | res: Tag{Omitempty: true}, 21 | }, 22 | { 23 | tag: "-", 24 | res: Tag{Name: "-"}, 25 | }, 26 | { 27 | tag: "hello,omitempty", 28 | res: Tag{Name: "hello", Omitempty: true}, 29 | }, 30 | { 31 | tag: "-,omitempty", 32 | res: Tag{Name: "-", Omitempty: true}, 33 | }, 34 | { 35 | tag: "hello,omitzero", 36 | res: Tag{Name: "hello", Omitzero: true}, 37 | }, 38 | { 39 | tag: "-,omitempty,omitzero", 40 | res: Tag{Name: "-", Omitempty: true, Omitzero: true}, 41 | }, 42 | } 43 | 44 | for _, test := range tests { 45 | t.Run(test.tag, func(t *testing.T) { 46 | if res := ParseTag(test.tag); res != test.res { 47 | t.Errorf("%s: %#v != %#v", test.tag, test.res, res) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestParseTagJSON(t *testing.T) { 54 | tests := []struct { 55 | tag string 56 | res Tag 57 | }{ 58 | { 59 | tag: "", 60 | res: Tag{}, 61 | }, 62 | { 63 | tag: "hello", 64 | res: Tag{Name: "hello"}, 65 | }, 66 | { 67 | tag: ",omitempty", 68 | res: Tag{Omitempty: true}, 69 | }, 70 | { 71 | tag: "-", 72 | res: Tag{Name: "-"}, 73 | }, 74 | { 75 | tag: "hello,omitempty", 76 | res: Tag{Name: "hello", Omitempty: true}, 77 | }, 78 | { 79 | tag: "-,omitempty", 80 | res: Tag{Name: "-", Omitempty: true}, 81 | }, 82 | } 83 | 84 | for _, test := range tests { 85 | t.Run(test.tag, func(t *testing.T) { 86 | if res := ParseTag(test.tag); res != test.res { 87 | t.Errorf("%s: %#v != %#v", test.tag, test.res, res) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /util/objconv/objutil/zero.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | import ( 4 | "reflect" 5 | "unsafe" 6 | ) 7 | 8 | // IsZero returns true if the value given as argument is the zero-value of 9 | // the type of v. 10 | func IsZero(v interface{}) bool { 11 | return IsZeroValue(reflect.ValueOf(v)) 12 | } 13 | 14 | func IsZeroValue(v reflect.Value) bool { 15 | if !v.IsValid() { 16 | return true // nil interface{} 17 | } 18 | switch v.Kind() { 19 | case reflect.Map, reflect.Slice, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func: 20 | return v.IsNil() 21 | case reflect.Bool: 22 | return !v.Bool() 23 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 24 | return v.Int() == 0 25 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 26 | return v.Uint() == 0 27 | case reflect.Float32, reflect.Float64: 28 | return v.Float() == 0 29 | case reflect.String: 30 | return v.Len() == 0 31 | case reflect.UnsafePointer: 32 | return unsafe.Pointer(v.Pointer()) == nil 33 | case reflect.Array: 34 | return isZeroArray(v) 35 | case reflect.Struct: 36 | return isZeroStruct(v) 37 | } 38 | return false 39 | } 40 | 41 | func isZeroArray(v reflect.Value) bool { 42 | for i, n := 0, v.Len(); i != n; i++ { 43 | if !IsZeroValue(v.Index(i)) { 44 | return false 45 | } 46 | } 47 | return true 48 | } 49 | 50 | func isZeroStruct(v reflect.Value) bool { 51 | for i, n := 0, v.NumField(); i != n; i++ { 52 | if !IsZeroValue(v.Field(i)) { 53 | return false 54 | } 55 | } 56 | return true 57 | } 58 | -------------------------------------------------------------------------------- /util/objconv/objutil/zero_test.go: -------------------------------------------------------------------------------- 1 | package objutil 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "unsafe" 7 | ) 8 | 9 | func TestIsZeroTrue(t *testing.T) { 10 | tests := []interface{}{ 11 | nil, 12 | 13 | false, 14 | 15 | int(0), 16 | int8(0), 17 | int16(0), 18 | int32(0), 19 | int64(0), 20 | 21 | uint(0), 22 | uint8(0), 23 | uint16(0), 24 | uint32(0), 25 | uint64(0), 26 | uintptr(0), 27 | 28 | float32(0), 29 | float64(0), 30 | 31 | "", 32 | []byte(nil), 33 | []int(nil), 34 | [...]int{}, 35 | [...]int{0, 0, 0}, 36 | 37 | (map[string]int)(nil), 38 | 39 | (*int)(nil), 40 | unsafe.Pointer(nil), 41 | 42 | struct{}{}, 43 | struct{ A int }{}, 44 | 45 | (chan struct{})(nil), 46 | (func())(nil), 47 | } 48 | 49 | for _, test := range tests { 50 | t.Run(fmt.Sprintf("%T", test), func(t *testing.T) { 51 | if !IsZero(test) { 52 | t.Errorf("%T, %#v should be an empty value", test, test) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func TestIsZeroFalse(t *testing.T) { 59 | answer := 42 60 | 61 | tests := []interface{}{ 62 | true, 63 | 64 | int(1), 65 | int8(1), 66 | int16(1), 67 | int32(1), 68 | int64(1), 69 | 70 | uint(1), 71 | uint8(1), 72 | uint16(1), 73 | uint32(1), 74 | uint64(1), 75 | uintptr(1), 76 | 77 | float32(1), 78 | float64(1), 79 | 80 | "Hello World!", 81 | []byte{}, 82 | []int{}, 83 | []int{1, 2, 3}, 84 | [...]int{1, 2, 3}, 85 | 86 | map[string]int{}, 87 | map[string]int{"answer": 42}, 88 | 89 | &answer, 90 | unsafe.Pointer(&answer), 91 | 92 | struct{ A int }{42}, 93 | 94 | make(chan struct{}), 95 | func() {}, 96 | } 97 | 98 | for _, test := range tests { 99 | t.Run(fmt.Sprintf("%T", test), func(t *testing.T) { 100 | if IsZero(test) { 101 | t.Errorf("%T, %#v should not be an empty value", test, test) 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /util/objconv/sort.go: -------------------------------------------------------------------------------- 1 | package objconv 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "sort" 7 | ) 8 | 9 | type sortIntValues []reflect.Value 10 | 11 | func (s sortIntValues) Len() int { return len(s) } 12 | func (s sortIntValues) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 13 | func (s sortIntValues) Less(i, j int) bool { return s[i].Int() < s[j].Int() } 14 | 15 | type sortUintValues []reflect.Value 16 | 17 | func (s sortUintValues) Len() int { return len(s) } 18 | func (s sortUintValues) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 19 | func (s sortUintValues) Less(i, j int) bool { return s[i].Uint() < s[j].Uint() } 20 | 21 | type sortFloatValues []reflect.Value 22 | 23 | func (s sortFloatValues) Len() int { return len(s) } 24 | func (s sortFloatValues) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 25 | func (s sortFloatValues) Less(i, j int) bool { return s[i].Float() < s[j].Float() } 26 | 27 | type sortStringValues []reflect.Value 28 | 29 | func (s sortStringValues) Len() int { return len(s) } 30 | func (s sortStringValues) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 31 | func (s sortStringValues) Less(i, j int) bool { return s[i].String() < s[j].String() } 32 | 33 | type sortBytesValues []reflect.Value 34 | 35 | func (s sortBytesValues) Len() int { return len(s) } 36 | func (s sortBytesValues) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 37 | func (s sortBytesValues) Less(i, j int) bool { 38 | return bytes.Compare(s[i].Bytes(), s[j].Bytes()) < 0 39 | } 40 | 41 | func sortValues(typ reflect.Type, v []reflect.Value) { 42 | switch typ.Kind() { 43 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 44 | sort.Sort(sortIntValues(v)) 45 | 46 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 47 | sort.Sort(sortUintValues(v)) 48 | 49 | case reflect.Float32, reflect.Float64: 50 | sort.Sort(sortFloatValues(v)) 51 | 52 | case reflect.String: 53 | sort.Sort(sortStringValues(v)) 54 | 55 | case reflect.Slice: 56 | if typ.Elem().Kind() == reflect.Uint8 { 57 | sort.Sort(sortBytesValues(v)) 58 | } 59 | } 60 | 61 | // For all other types we give up on trying to sort the values, 62 | // anyway it's likely not gonna be a serializable type, or something 63 | // that doesn't make sense. 64 | } 65 | -------------------------------------------------------------------------------- /util/objconv/struct_test.go: -------------------------------------------------------------------------------- 1 | package objconv 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestMakeStructField(t *testing.T) { 9 | type A struct{ A int } 10 | type a struct{ a int } 11 | type B struct{ A } 12 | type b struct{ a } 13 | 14 | tests := []struct { 15 | s reflect.StructField 16 | f structField 17 | }{ 18 | { 19 | s: reflect.TypeOf(A{}).Field(0), 20 | f: structField{ 21 | index: []int{0}, 22 | name: "A", 23 | }, 24 | }, 25 | 26 | { 27 | s: reflect.TypeOf(a{}).Field(0), 28 | f: structField{ 29 | index: []int{0}, 30 | name: "a", 31 | }, 32 | }, 33 | 34 | { 35 | s: reflect.TypeOf(B{}).Field(0), 36 | f: structField{ 37 | index: []int{0}, 38 | name: "A", 39 | }, 40 | }, 41 | 42 | { 43 | s: reflect.TypeOf(b{a: a{}}).Field(0), 44 | f: structField{ 45 | index: []int{0}, 46 | name: "a", 47 | }, 48 | }, 49 | } 50 | 51 | for _, test := range tests { 52 | t.Run("", func(t *testing.T) { 53 | f := makeStructField(test.s, map[reflect.Type]*structType{}) 54 | f.decode = nil // function types are not comparable 55 | f.encode = nil 56 | 57 | if !reflect.DeepEqual(test.f, f) { 58 | t.Errorf("%#v != %#v", test.f, f) 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestMustValueOf(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | in interface{} 16 | out interface{} 17 | panic bool 18 | }{ 19 | { 20 | name: "should not panic", 21 | in: 42, 22 | out: ValueOf(42), 23 | }, 24 | { 25 | name: "should panic", 26 | in: struct{}{}, 27 | panic: true, 28 | }, 29 | } 30 | for _, test := range tests { 31 | t.Run(test.name, func(t *testing.T) { 32 | if test.panic { 33 | require.PanicsWithValue(t, "stats.MustValueOf received a value of unsupported type", func() { 34 | MustValueOf(ValueOf(test.in)) 35 | }) 36 | } else { 37 | out := MustValueOf(ValueOf(test.in)) 38 | require.EqualValues(t, test.out, out) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestValueOfIdentity(t *testing.T) { 45 | v1 := ValueOf(3.14) 46 | v2 := ValueOf(v1) 47 | if !reflect.DeepEqual(v1, v2) { 48 | t.Fatalf("Expected %+v to be equal to %+v", v2, v1) 49 | } 50 | } 51 | 52 | func TestValueOf(t *testing.T) { 53 | tests := []struct { 54 | in interface{} 55 | out interface{} 56 | }{ 57 | {nil, nil}, 58 | {true, true}, 59 | {false, false}, 60 | {int8(1), int64(1)}, 61 | {int8(-1), int64(-1)}, 62 | {int16(1), int64(1)}, 63 | {int16(-1), int64(-1)}, 64 | {int32(1), int64(1)}, 65 | {int32(-1), int64(-1)}, 66 | {int64(1), int64(1)}, 67 | {int64(-1), int64(-1)}, 68 | {uint8(1), uint64(1)}, 69 | {uint16(1), uint64(1)}, 70 | {uint32(1), uint64(1)}, 71 | {uint64(1), uint64(1)}, 72 | {uintptr(1), uint64(1)}, 73 | {float32(0.5), float64(0.5)}, 74 | {float64(0.5), float64(0.5)}, 75 | {time.Second, time.Second}, 76 | } 77 | 78 | for _, test := range tests { 79 | t.Run(fmt.Sprintf("%T(%v)", test.in, test.in), func(t *testing.T) { 80 | v := ValueOf(test.in).Interface() 81 | 82 | if !reflect.DeepEqual(v, test.out) { 83 | t.Errorf("bad value: %T(%v)", v, v) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func BenchmarkValueOf(b *testing.B) { 90 | for i := 0; i != b.N; i++ { 91 | ValueOf(42) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /veneur/client.go: -------------------------------------------------------------------------------- 1 | package veneur 2 | 3 | import ( 4 | "time" 5 | 6 | stats "github.com/segmentio/stats/v5" 7 | "github.com/segmentio/stats/v5/datadog" 8 | ) 9 | 10 | // Const Sink Configuration types. 11 | const ( 12 | GlobalOnly = "veneurglobalonly" 13 | LocalOnly = "veneurlocalonly" 14 | SinkOnly = "veneursinkonly" 15 | 16 | SignalfxSink = "signalfx" 17 | DatadogSink = "datadog" 18 | KafkaSink = "kafka" 19 | ) 20 | 21 | // SinkOnly tags. 22 | var ( 23 | TagSignalfxOnly = stats.Tag{Name: SinkOnly, Value: SignalfxSink} 24 | TagDatadogOnly = stats.Tag{Name: SinkOnly, Value: DatadogSink} 25 | TagKafkaOnly = stats.Tag{Name: SinkOnly, Value: KafkaSink} 26 | ) 27 | 28 | // The ClientConfig type is used to configure veneur clients. 29 | // It inherits the datadog config since the veneur client reuses 30 | // the logic in the datadog client to emit metrics. 31 | type ClientConfig struct { 32 | datadog.ClientConfig 33 | 34 | // Veneur Specific Configuration 35 | // If set true, all metrics will be sent with veneurglobalonly tag 36 | GlobalOnly bool 37 | 38 | // If set true, all metrics will be sent with veneurlocalonly tag 39 | // Cannot be set in conjunction with GlobalOnly 40 | LocalOnly bool 41 | 42 | // Adds veneursinkonly: tag to all metrics. Valid sinks can be 43 | // found here: https://github.com/stripe/veneur#routing-metrics 44 | SinksOnly []string 45 | } 46 | 47 | // Client represents an veneur client that implements the stats.Handler 48 | // interface. 49 | type Client struct { 50 | *datadog.Client 51 | tags []stats.Tag 52 | } 53 | 54 | // NewClient creates and returns a new veneur client publishing metrics to the 55 | // server running at addr. 56 | func NewClient(addr string) *Client { 57 | return NewClientWith(ClientConfig{ClientConfig: datadog.ClientConfig{Address: addr}}) 58 | } 59 | 60 | // NewClientGlobal creates a client that sends all metrics to the Global Veneur Aggregator. 61 | func NewClientGlobal(addr string) *Client { 62 | return NewClientWith(ClientConfig{ClientConfig: datadog.ClientConfig{Address: addr}, GlobalOnly: true}) 63 | } 64 | 65 | // NewClientWith creates and returns a new veneur client configured with the 66 | // given config. 67 | func NewClientWith(config ClientConfig) *Client { 68 | // Construct Veneur-specific Tags we will append to measures 69 | tags := []stats.Tag{} 70 | if config.GlobalOnly { 71 | tags = append(tags, stats.Tag{Name: GlobalOnly}) 72 | } else if config.LocalOnly { 73 | tags = append(tags, stats.Tag{Name: LocalOnly}) 74 | } 75 | for _, t := range config.SinksOnly { 76 | tags = append(tags, stats.Tag{Name: SinkOnly, Value: t}) 77 | } 78 | 79 | return &Client{ 80 | Client: datadog.NewClientWith(datadog.ClientConfig{ 81 | Address: config.Address, 82 | BufferSize: config.BufferSize, 83 | Filters: config.Filters, 84 | }), 85 | tags: tags, 86 | } 87 | } 88 | 89 | // HandleMeasures satisfies the stats.Handler interface. 90 | func (c *Client) HandleMeasures(time time.Time, measures ...stats.Measure) { 91 | // If there are no tags to add, call HandleMeasures with measures directly 92 | if len(c.tags) == 0 { 93 | c.Client.HandleMeasures(time, measures...) 94 | return 95 | } 96 | 97 | finalMeasures := make([]stats.Measure, len(measures)) 98 | for i := range measures { 99 | finalMeasures[i] = measures[i].Clone() 100 | finalMeasures[i].Tags = append(measures[i].Tags, c.tags...) 101 | } 102 | 103 | c.Client.HandleMeasures(time, finalMeasures...) 104 | } 105 | -------------------------------------------------------------------------------- /veneur/client_test.go: -------------------------------------------------------------------------------- 1 | package veneur 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/segmentio/stats/v5" 8 | ) 9 | 10 | func TestClient(t *testing.T) { 11 | client := NewClientWith(ClientConfig{ 12 | GlobalOnly: true, 13 | SinksOnly: []string{SignalfxSink}, 14 | }) 15 | 16 | for i := 0; i != 1000; i++ { 17 | client.HandleMeasures(time.Time{}, stats.Measure{ 18 | Name: "request", 19 | Fields: []stats.Field{ 20 | {Name: "count", Value: stats.ValueOf(5)}, 21 | {Name: "rtt", Value: stats.ValueOf(100 * time.Millisecond)}, 22 | }, 23 | Tags: []stats.Tag{ 24 | TagSignalfxOnly, 25 | }, 26 | }) 27 | } 28 | 29 | if err := client.Close(); err != nil { 30 | t.Error(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | const Version = "5.6.1" 10 | 11 | var ( 12 | vsnOnce sync.Once 13 | vsn string 14 | ) 15 | 16 | func isDevel(vsn string) bool { 17 | return strings.Count(vsn, " ") > 2 || strings.HasPrefix(vsn, "devel") 18 | } 19 | 20 | // GoVersion reports the Go version, in a format that is consumable by metrics 21 | // tools. 22 | func GoVersion() string { 23 | vsnOnce.Do(func() { 24 | vsn = strings.TrimPrefix(runtime.Version(), "go") 25 | }) 26 | return vsn 27 | } 28 | 29 | // DevelGoVersion reports whether the version of Go that compiled or ran this 30 | // library is a development ("tip") version. This is useful to distinguish 31 | // because tip versions include a commit SHA and change frequently, so are less 32 | // useful for metric reporting. 33 | func DevelGoVersion() bool { 34 | return isDevel(GoVersion()) 35 | } 36 | -------------------------------------------------------------------------------- /version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "testing" 4 | 5 | func TestDevel(t *testing.T) { 6 | if isDevel("1.23.4") { 7 | t.Errorf("expected 1.23.4 to return false; got true") 8 | } 9 | if !isDevel("devel go1.24-d1d9312950 Wed Jan 1 21:18:59 2025 -0800 darwin/arm64") { 10 | t.Errorf("expected tip version to return true; got false") 11 | } 12 | } 13 | --------------------------------------------------------------------------------