├── network ├── testdata │ ├── packet1.bin │ └── packet2.bin ├── fuzz_test.go ├── client_test.go ├── main.go ├── fuzz.go ├── server_test.go ├── client.go ├── network_x_test.go ├── crypto_test.go ├── server.go ├── buffer_test.go ├── crypto.go ├── parse.go └── buffer.go ├── check_fmt.sh ├── .gitignore ├── go.mod ├── plugin ├── log_test.go ├── fake │ ├── interval.go │ ├── fake.go │ ├── shutdown.go │ ├── log.go │ ├── write.go │ └── read.go ├── README.md ├── c.go ├── config.go ├── log.go ├── generator │ └── generator.go └── wrapper.go ├── LICENSE ├── .travis.yml ├── rpc ├── server.go ├── rpc.go ├── client.go ├── marshalling.go └── proto │ ├── collectd.pb.go │ └── types │ └── types.pb.go ├── README.md ├── exec ├── exec_test.go ├── exec_x_test.go └── exec.go ├── format ├── putval.go ├── graphite.go ├── graphite_test.go └── putval_test.go ├── go.sum ├── api ├── json_test.go ├── json.go ├── types_test.go ├── types.go ├── main.go └── main_test.go ├── cdtime ├── cdtime.go └── cdtime_test.go ├── export ├── export_test.go └── export.go └── meta ├── meta.go └── meta_test.go /network/testdata/packet1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectd/go-collectd/HEAD/network/testdata/packet1.bin -------------------------------------------------------------------------------- /network/testdata/packet2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectd/go-collectd/HEAD/network/testdata/packet2.bin -------------------------------------------------------------------------------- /check_fmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | declare -r NEED_FMT="$(gofmt -l **/*.go)" 4 | 5 | if [[ -z "${NEED_FMT}" ]]; then 6 | exit 0 7 | fi 8 | 9 | echo "The following files are NOT formatted with gofmt:" 10 | echo "${NEED_FMT}" 11 | exit 1 12 | -------------------------------------------------------------------------------- /.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 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /network/fuzz_test.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | 3 | package network // import "collectd.org/network" 4 | 5 | import ( 6 | "io/ioutil" 7 | "testing" 8 | ) 9 | 10 | func TestFuzz(t *testing.T) { 11 | data, err := ioutil.ReadFile("testdata/packet2.bin") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | got := Fuzz(data) 17 | if got != 1 { 18 | t.Errorf("Failed to fuzz a sample packet. Wanted [%v] Got [%v]\n", 1, got) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module collectd.org 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/golang/protobuf v1.5.3 7 | github.com/google/go-cmp v0.6.0 8 | go.uber.org/multierr v1.11.0 9 | golang.org/x/net v0.19.0 10 | google.golang.org/grpc v1.60.1 11 | ) 12 | 13 | require ( 14 | golang.org/x/sys v0.15.0 // indirect 15 | golang.org/x/text v0.14.0 // indirect 16 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect 17 | google.golang.org/protobuf v1.31.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /plugin/log_test.go: -------------------------------------------------------------------------------- 1 | package plugin_test 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | 8 | "collectd.org/plugin" 9 | ) 10 | 11 | func ExampleLogWriter() { 12 | l := log.New(plugin.LogWriter(plugin.SeverityError), "", log.Lshortfile) 13 | 14 | // Start an HTTP server that logs errors to collectd's logging facility. 15 | srv := &http.Server{ 16 | ErrorLog: l, 17 | } 18 | if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 19 | l.Println("ListenAndServe:", err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugin/fake/interval.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 4 | // #cgo LDFLAGS: -ldl 5 | // #include 6 | // #include "plugin.h" 7 | // 8 | // static cdtime_t interval = TIME_T_TO_CDTIME_T_STATIC(10); 9 | // cdtime_t plugin_get_interval(void) { 10 | // return interval; 11 | // } 12 | // void plugin_set_interval(cdtime_t d) { 13 | // interval = d; 14 | // } 15 | import "C" 16 | 17 | import ( 18 | "time" 19 | 20 | "collectd.org/cdtime" 21 | ) 22 | 23 | // SetInterval sets the interval returned by the fake plugin_get_interval() 24 | // function. 25 | func SetInterval(d time.Duration) { 26 | ival := cdtime.NewDuration(d) 27 | C.plugin_set_interval(C.cdtime_t(ival)) 28 | } 29 | -------------------------------------------------------------------------------- /network/client_test.go: -------------------------------------------------------------------------------- 1 | package network // import "collectd.org/network" 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "time" 8 | 9 | "collectd.org/api" 10 | ) 11 | 12 | func ExampleClient() { 13 | ctx := context.Background() 14 | conn, err := Dial(net.JoinHostPort("example.com", DefaultService), ClientOptions{}) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | defer conn.Close() 19 | 20 | vl := &api.ValueList{ 21 | Identifier: api.Identifier{ 22 | Host: "example.com", 23 | Plugin: "golang", 24 | Type: "gauge", 25 | }, 26 | Time: time.Now(), 27 | Interval: 10 * time.Second, 28 | Values: []api.Value{api.Gauge(42.0)}, 29 | } 30 | 31 | if err := conn.Write(ctx, vl); err != nil { 32 | log.Fatal(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2014 Kimo Rosenbaum 4 | Copyright (c) 2015-2016 Florian Forster 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /plugin/fake/fake.go: -------------------------------------------------------------------------------- 1 | // Package fake implements fake versions of the C functions imported from the 2 | // collectd daemon for testing. 3 | package fake 4 | 5 | // void reset_log(void); 6 | // void reset_read(void); 7 | // void reset_shutdown(void); 8 | // void reset_write(void); 9 | // 10 | // int timeout_g = 2; 11 | import "C" 12 | 13 | import ( 14 | "time" 15 | ) 16 | 17 | // TearDown cleans up after a test and prepares shared resources for the next 18 | // test. 19 | // 20 | // Note that this only resets the state of the fake implementations, such as 21 | // "plugin_register_log()". The Go code in "collectd.org/plugin" may still hold 22 | // a reference to the callback even after this function has been called. 23 | func TearDown() { 24 | SetInterval(10 * time.Second) 25 | C.reset_log() 26 | C.reset_read() 27 | C.reset_shutdown() 28 | C.reset_write() 29 | } 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Request newer Ubuntu version. Xenial (current default) ships collectd 5.5, which is too old. 2 | dist: bionic 3 | language: go 4 | 5 | go: 6 | - "stable" 7 | # - "oldstable" # https://github.com/travis-ci/gimme/issues/179 8 | - "master" 9 | 10 | before_install: 11 | - sudo apt-get -y install collectd-dev 12 | 13 | env: 14 | - CGO_ENABLED=1 CGO_CPPFLAGS="-I/usr/include/collectd/core/daemon -I/usr/include/collectd/core -I/usr/include/collectd" 15 | 16 | go_import_path: collectd.org 17 | 18 | matrix: 19 | allow_failures: 20 | - go: master 21 | 22 | before_script: 23 | - go get golang.org/x/lint/golint 24 | 25 | script: 26 | - go test -coverprofile=/dev/null ./... 27 | - ./check_fmt.sh 28 | - go vet ./... 29 | - golint -set_exit_status {cdtime,config,exec,export,format,meta,network}/... plugin/ rpc/ 30 | 31 | # TODO(octo): run the fuzz test: 32 | # - go test -v -tags gofuzz ./... 33 | -------------------------------------------------------------------------------- /plugin/README.md: -------------------------------------------------------------------------------- 1 | ## collectd plugins in Go 2 | 3 | ## About 4 | 5 | This is _experimental_ code to write _collectd_ plugins in Go. That means the 6 | API is not yet stable. It requires Go 1.13 or later and a recent version of the 7 | collectd sources to build. 8 | 9 | ## Build 10 | 11 | To set up your build environment, set the `CGO_CPPFLAGS` environment variable 12 | so that _cgo_ can find the required header files: 13 | 14 | export COLLECTD_SRC="/path/to/collectd" 15 | export CGO_CPPFLAGS="-I${COLLECTD_SRC}/src/daemon -I${COLLECTD_SRC}/src" 16 | 17 | You can then compile your Go plugins with: 18 | 19 | go build -buildmode=c-shared -o example.so 20 | 21 | More information is available in the documentation of the `collectd.org/plugin` 22 | package. 23 | 24 | godoc collectd.org/plugin 25 | 26 | ## Future 27 | 28 | Only *log*, *read*, *write*, and *shutdown* callbacks are currently supported. 29 | Based on these implementations it should be possible to implement the remaining 30 | callbacks, even with little prior Cgo experience. The *init*, *flush*, and 31 | *missing* callbacks are all likely low-hanging fruit. The *notification* 32 | callback is a bit trickier because it requires implementing notifications in 33 | the `collectd.org/api` package and the (un)marshaling of `notification_t`. The 34 | (complex) *config* callback is currently work in progress, see #30. 35 | 36 | If you're willing to give any of this a shot, please ping @octo to avoid 37 | duplicate work. 38 | -------------------------------------------------------------------------------- /network/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package network implements collectd's binary network protocol. 3 | */ 4 | package network // import "collectd.org/network" 5 | 6 | // Well-known addresses and port. 7 | const ( 8 | DefaultIPv4Address = "239.192.74.66" 9 | DefaultIPv6Address = "ff18::efc0:4a42" 10 | DefaultService = "25826" 11 | DefaultPort = 25826 12 | ) 13 | 14 | // DefaultBufferSize is the default size of "Buffer". This is based on the 15 | // maximum bytes that fit into an Ethernet frame without fragmentation: 16 | // - ( + ) = 1500 - (40 + 8) = 1452 17 | const DefaultBufferSize = 1452 18 | 19 | // Numeric data source type identifiers. 20 | const ( 21 | dsTypeCounter = 0 22 | dsTypeGauge = 1 23 | dsTypeDerive = 2 24 | ) 25 | 26 | // IDs of the various "parts", i.e. subcomponents of a packet. 27 | const ( 28 | typeHost = 0x0000 29 | typeTime = 0x0001 30 | typeTimeHR = 0x0008 31 | typePlugin = 0x0002 32 | typePluginInstance = 0x0003 33 | typeType = 0x0004 34 | typeTypeInstance = 0x0005 35 | typeValues = 0x0006 36 | typeInterval = 0x0007 37 | typeIntervalHR = 0x0009 38 | typeSignSHA256 = 0x0200 39 | typeEncryptAES256 = 0x0210 40 | ) 41 | 42 | // SecurityLevel determines whether data is signed, encrypted or used without 43 | // any protection. 44 | type SecurityLevel int 45 | 46 | // Predefined security levels. "None" is used for plain text. 47 | const ( 48 | None SecurityLevel = iota 49 | Sign 50 | Encrypt 51 | ) 52 | -------------------------------------------------------------------------------- /plugin/fake/shutdown.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 4 | // #cgo LDFLAGS: -ldl 5 | // #include 6 | // #include "plugin.h" 7 | // 8 | // typedef struct { 9 | // const char *name; 10 | // plugin_shutdown_cb callback; 11 | // } shutdown_callback_t; 12 | // static shutdown_callback_t *shutdown_callbacks = NULL; 13 | // static size_t shutdown_callbacks_num = 0; 14 | // 15 | // int plugin_register_shutdown(const char *name, plugin_shutdown_cb callback) { 16 | // shutdown_callback_t *ptr = 17 | // realloc(shutdown_callbacks, 18 | // (shutdown_callbacks_num + 1) * sizeof(*shutdown_callbacks)); 19 | // if (ptr == NULL) { 20 | // return ENOMEM; 21 | // } 22 | // shutdown_callbacks = ptr; 23 | // shutdown_callbacks[shutdown_callbacks_num] = (shutdown_callback_t){ 24 | // .name = name, 25 | // .callback = callback, 26 | // }; 27 | // shutdown_callbacks_num++; 28 | // 29 | // return 0; 30 | // } 31 | // 32 | // int plugin_shutdown_all(void) { 33 | // int ret = 0; 34 | // for (size_t i = 0; i < shutdown_callbacks_num; i++) { 35 | // int err = shutdown_callbacks[i].callback(); 36 | // if (err != 0) { 37 | // ret = err; 38 | // } 39 | // } 40 | // return ret; 41 | // } 42 | // 43 | // void reset_shutdown(void) { 44 | // free(shutdown_callbacks); 45 | // shutdown_callbacks = NULL; 46 | // shutdown_callbacks_num = 0; 47 | // } 48 | import "C" 49 | 50 | import ( 51 | "fmt" 52 | ) 53 | 54 | // ShutdownAll calls all registered shutdown callbacks. 55 | func ShutdownAll() error { 56 | status, err := C.plugin_shutdown_all() 57 | if err != nil { 58 | return err 59 | } 60 | if status != 0 { 61 | return fmt.Errorf("plugin_shutdown_all() = %d", status) 62 | } 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /network/fuzz.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | 3 | package network // import "collectd.org/network" 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | ) 10 | 11 | type fuzzLookup struct { 12 | user, password string 13 | } 14 | 15 | func (fl fuzzLookup) Password(user string) (string, error) { 16 | if fl.user == user { 17 | return fl.password, nil 18 | } 19 | return "", fmt.Errorf("no such user: %q", user) 20 | } 21 | 22 | // Fuzz is used by the https://github.com/dvyukov/go-fuzz framework 23 | // It's method signature must match the prescribed format and it is expected to panic upon failure 24 | // Usage: 25 | // 26 | // $ go-fuzz-build collectd.org/network 27 | // $ mkdir -p /tmp/fuzzwork/corpus 28 | // $ cp network/testdata/packet1.bin /tmp/fuzzwork/corpus 29 | // $ go-fuzz -bin=./network-fuzz.zip -workdir=/tmp/fuzzwork 30 | func Fuzz(data []byte) int { 31 | ctx := context.Background() 32 | 33 | // deserialize 34 | d1, err := Parse(data, ParseOpts{ 35 | PasswordLookup: fuzzLookup{ 36 | user: "test", 37 | password: "test", 38 | }, 39 | }) 40 | if err != nil || len(d1) == 0 { 41 | return 0 42 | } 43 | 44 | // serialize 45 | s1 := NewBuffer(0) 46 | if err := s1.Write(ctx, d1[0]); err != nil { 47 | panic(err) 48 | } 49 | 50 | // deserialize 51 | d2, err := Parse(s1.buffer.Bytes(), ParseOpts{}) 52 | if err != nil { 53 | return 0 54 | } 55 | if len(d2) == 0 { 56 | panic("d2 is empty but no err was returned") 57 | } 58 | 59 | // serialize 60 | s2 := NewBuffer(0) 61 | if err := s2.Write(ctx, d2[0]); err != nil { 62 | panic(err) 63 | } 64 | 65 | if bytes.Compare(s1.buffer.Bytes(), s2.buffer.Bytes()) != 0 { 66 | panic(fmt.Sprintf("Comparison of two serialized versions failed s1 [%v] s2[%v]", s1.buffer.Bytes(), s2.buffer.Bytes())) 67 | } 68 | 69 | return 1 70 | } 71 | -------------------------------------------------------------------------------- /plugin/fake/log.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 4 | // #cgo LDFLAGS: -ldl 5 | // #include 6 | // #include "plugin.h" 7 | // 8 | // typedef struct { 9 | // const char *name; 10 | // plugin_log_cb callback; 11 | // user_data_t user_data; 12 | // } log_callback_t; 13 | // static log_callback_t *log_callbacks = NULL; 14 | // static size_t log_callbacks_num = 0; 15 | // 16 | // int plugin_register_log(const char *name, 17 | // plugin_log_cb callback, 18 | // user_data_t const *user_data) { 19 | // log_callback_t *ptr = realloc(log_callbacks, (log_callbacks_num+1) * sizeof(*log_callbacks)); 20 | // if (ptr == NULL) { 21 | // return ENOMEM; 22 | // } 23 | // log_callbacks = ptr; 24 | // log_callbacks[log_callbacks_num] = (log_callback_t){ 25 | // .name = name, 26 | // .callback = callback, 27 | // .user_data = *user_data, 28 | // }; 29 | // log_callbacks_num++; 30 | // 31 | // return 0; 32 | // } 33 | // 34 | // void plugin_log(int level, const char *format, ...) { 35 | // char msg[1024]; 36 | // va_list ap; 37 | // va_start(ap, format); 38 | // vsnprintf(msg, sizeof(msg), format, ap); 39 | // msg[sizeof(msg)-1] = 0; 40 | // va_end(ap); 41 | // 42 | // for (size_t i = 0; i < log_callbacks_num; i++) { 43 | // log_callbacks[i].callback(level, msg, &log_callbacks[i].user_data); 44 | // } 45 | // } 46 | // 47 | // void reset_log(void) { 48 | // for (size_t i = 0; i < log_callbacks_num; i++) { 49 | // user_data_t *ud = &log_callbacks[i].user_data; 50 | // if (ud->free_func == NULL) { 51 | // continue; 52 | // } 53 | // ud->free_func(ud->data); 54 | // ud->data = NULL; 55 | // } 56 | // free(log_callbacks); 57 | // log_callbacks = NULL; 58 | // log_callbacks_num = 0; 59 | // } 60 | import "C" 61 | -------------------------------------------------------------------------------- /network/server_test.go: -------------------------------------------------------------------------------- 1 | package network // import "collectd.org/network" 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "net" 8 | "os" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "collectd.org/format" 14 | ) 15 | 16 | // This example demonstrates how to listen to encrypted network traffic and 17 | // dump it to STDOUT using format.Putval. 18 | func ExampleServer_ListenAndWrite() { 19 | srv := &Server{ 20 | Addr: net.JoinHostPort("::", DefaultService), 21 | Writer: format.NewPutval(os.Stdout), 22 | PasswordLookup: NewAuthFile("/etc/collectd/users"), 23 | } 24 | 25 | // blocks 26 | log.Fatal(srv.ListenAndWrite(context.Background())) 27 | } 28 | 29 | // This example demonstrates how to forward received IPv6 multicast traffic to 30 | // a unicast address, using PSK encryption. 31 | func ExampleListenAndWrite() { 32 | opts := ClientOptions{ 33 | SecurityLevel: Encrypt, 34 | Username: "collectd", 35 | Password: "dieXah7e", 36 | } 37 | client, err := Dial(net.JoinHostPort("example.com", DefaultService), opts) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | defer client.Close() 42 | 43 | // blocks 44 | log.Fatal(ListenAndWrite(context.Background(), ":"+DefaultService, client)) 45 | } 46 | 47 | func TestServer_Cancellation(t *testing.T) { 48 | ctx, cancel := context.WithCancel(context.Background()) 49 | 50 | wg := &sync.WaitGroup{} 51 | wg.Add(1) 52 | 53 | var srvErr error 54 | go func() { 55 | srv := &Server{ 56 | Addr: "localhost:0", 57 | } 58 | 59 | srvErr = srv.ListenAndWrite(ctx) 60 | wg.Done() 61 | }() 62 | 63 | // wait for a bit, then shut down the server 64 | time.Sleep(100 * time.Millisecond) 65 | cancel() 66 | wg.Wait() 67 | 68 | if !errors.Is(srvErr, context.Canceled) { 69 | t.Errorf("srvErr = %v, want %v", srvErr, context.Canceled) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rpc/server.go: -------------------------------------------------------------------------------- 1 | package rpc // import "collectd.org/rpc" 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | pb "collectd.org/rpc/proto" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/codes" 10 | ) 11 | 12 | // RegisterServer registers the implementation srv with the gRPC instance s. 13 | func RegisterServer(s *grpc.Server, srv Interface) { 14 | pb.RegisterCollectdServer(s, &server{ 15 | Interface: srv, 16 | }) 17 | } 18 | 19 | // Type server implements pb.CollectdServer using the Go implementation of 20 | // rpc.Interface. 21 | type server struct { 22 | Interface 23 | } 24 | 25 | // PutValues reads ValueLists from stream and calls the Write() implementation 26 | // on each one. 27 | func (s *server) PutValues(stream pb.Collectd_PutValuesServer) error { 28 | for { 29 | req, err := stream.Recv() 30 | if err == io.EOF { 31 | break 32 | } 33 | if err != nil { 34 | return err 35 | } 36 | 37 | vl, err := UnmarshalValueList(req.GetValueList()) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if err := s.Write(stream.Context(), vl); err != nil { 43 | return grpc.Errorf(codes.Internal, "Write(%v): %v", vl, err) 44 | } 45 | } 46 | 47 | return stream.SendAndClose(&pb.PutValuesResponse{}) 48 | } 49 | 50 | // QueryValues calls the Query() implementation and streams all ValueLists from 51 | // the channel back to the client. 52 | func (s *server) QueryValues(req *pb.QueryValuesRequest, stream pb.Collectd_QueryValuesServer) error { 53 | id := UnmarshalIdentifier(req.GetIdentifier()) 54 | 55 | ctx, cancel := context.WithCancel(stream.Context()) 56 | defer cancel() 57 | 58 | ch, err := s.Query(ctx, id) 59 | if err != nil { 60 | return grpc.Errorf(codes.Internal, "Query(%v): %v", id, err) 61 | } 62 | 63 | for vl := range ch { 64 | pbVL, err := MarshalValueList(vl) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | res := &pb.QueryValuesResponse{ 70 | ValueList: pbVL, 71 | } 72 | if err := stream.Send(res); err != nil { 73 | return err 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /rpc/rpc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package rpc implements an idiomatic Go interface to collectd's gRPC server. 3 | 4 | The functions and types in this package aim to make it easy and convenient to 5 | use collectd's gRPC interface. It supports both client and server code. 6 | 7 | Client code 8 | 9 | Synopsis: 10 | 11 | conn, err := grpc.Dial(*addr, opts...) 12 | if err != nil { 13 | // handle error 14 | } 15 | 16 | c := rpc.NewClient(conn) 17 | 18 | // Send a ValueList to the server. 19 | if err := c.Write(context.Background(), vl); err != nil { 20 | // handle error 21 | } 22 | 23 | // Retrieve matching ValueLists from the server. 24 | ch, err := c.Query(context.Background(), api.Identifier{ 25 | Host: "*", 26 | Plugin: "golang", 27 | }) 28 | if err != nil { 29 | // handle error 30 | } 31 | 32 | for vl := range ch { 33 | // consume ValueList 34 | } 35 | 36 | Server code 37 | 38 | Synopsis: 39 | 40 | type myServer struct { 41 | rpc.Interface 42 | } 43 | 44 | func (s *myServer) Write(ctx context.Context, vl *api.ValueList) error { 45 | // implementation 46 | } 47 | 48 | func (s *myServer) Query(ctx context.Context, id *api.Identifier) (<-chan *api.ValueList, error) { 49 | // implementation 50 | } 51 | 52 | func main() { 53 | sock, err := net.Listen("tcp", ":12345") 54 | if err != nil { 55 | // handle error 56 | } 57 | 58 | srv := grpc.NewServer(opts...) 59 | rpc.RegisterServer(srv, &myServer{}) 60 | srv.Serve(sock) 61 | } 62 | */ 63 | package rpc // import "collectd.org/rpc" 64 | 65 | import ( 66 | "context" 67 | 68 | "collectd.org/api" 69 | ) 70 | 71 | // Interface is an idiomatic Go interface for the Collectd gRPC service. 72 | // 73 | // To implement a client, pass a client connection to NewClient() to get back 74 | // an object implementing this interface. 75 | // 76 | // To implement a server, use RegisterServer() to hook an object, which 77 | // implements Interface, up to a gRPC server. 78 | type Interface interface { 79 | api.Writer 80 | Query(context.Context, *api.Identifier) (<-chan *api.ValueList, error) 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-collectd 2 | 3 | Utilities for using [collectd](https://collectd.org/) together with [Go](http://golang.org/). 4 | 5 | # Synopsis 6 | 7 | package main 8 | 9 | import ( 10 | "context" 11 | "time" 12 | 13 | "collectd.org/api" 14 | "collectd.org/exec" 15 | ) 16 | 17 | func main() { 18 | vl := api.ValueList{ 19 | Identifier: api.Identifier{ 20 | Host: exec.Hostname(), 21 | Plugin: "golang", 22 | Type: "gauge", 23 | }, 24 | Time: time.Now(), 25 | Interval: exec.Interval(), 26 | Values: []api.Value{api.Gauge(42)}, 27 | } 28 | exec.Putval.Write(context.Background(), &vl) 29 | } 30 | 31 | # Description 32 | 33 | This is a very simple package and very much a *Work in Progress*, so expect 34 | things to move around and be renamed a lot. 35 | 36 | The repository is organized as follows: 37 | 38 | * Package `collectd.org/api` declares data structures you may already know from 39 | the *collectd* source code itself, such as `ValueList`. 40 | * Package `collectd.org/exec` declares some utilities for writing binaries to 41 | be executed with the *exec plugin*. It provides some utilities (getting the 42 | hostname, e.g.) and an executor which you may use to easily schedule function 43 | calls. 44 | * Package `collectd.org/format` declares functions for formatting *ValueLists* 45 | in other format. Right now, only `PUTVAL` is implemented. Eventually I plan 46 | to add parsers for some formats, such as the JSON export. 47 | * Package `collectd.org/network` implements collectd's 48 | [binary network protocol](https://collectd.org/wiki/index.php/Binary_protocol). 49 | It offers client and server implementations, see `network.Client` and 50 | `network.ListenAndWrite()` for more details. 51 | 52 | # Install 53 | 54 | To use this package in your own programs, simply use `go get` to fetch the 55 | packages you need, for example: 56 | 57 | go get collectd.org/api 58 | 59 | # Author 60 | 61 | Florian "octo" Forster <ff at octo.it> 62 | -------------------------------------------------------------------------------- /rpc/client.go: -------------------------------------------------------------------------------- 1 | package rpc // import "collectd.org/rpc" 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | 8 | "collectd.org/api" 9 | pb "collectd.org/rpc/proto" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | // Type client implements rpc.Interface using a gRPC stub. 14 | type client struct { 15 | pb.CollectdClient 16 | } 17 | 18 | // NewClient returns a wrapper around the gRPC client connection that maps 19 | // between the Go interface and the gRPC interface. 20 | func NewClient(conn *grpc.ClientConn) Interface { 21 | return &client{ 22 | CollectdClient: pb.NewCollectdClient(conn), 23 | } 24 | } 25 | 26 | // Query maps its arguments to a QueryValuesRequest object and calls 27 | // QueryValues. The response is parsed by a goroutine and written to the 28 | // returned channel. 29 | func (c *client) Query(ctx context.Context, id *api.Identifier) (<-chan *api.ValueList, error) { 30 | stream, err := c.QueryValues(ctx, &pb.QueryValuesRequest{ 31 | Identifier: MarshalIdentifier(id), 32 | }) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | ch := make(chan *api.ValueList, 16) 38 | 39 | go func() { 40 | defer close(ch) 41 | 42 | for { 43 | res, err := stream.Recv() 44 | if err == io.EOF { 45 | break 46 | } 47 | if err != nil { 48 | log.Printf("error while receiving value lists: %v", err) 49 | return 50 | } 51 | 52 | vl, err := UnmarshalValueList(res.GetValueList()) 53 | if err != nil { 54 | log.Printf("received malformed response: %v", err) 55 | continue 56 | } 57 | 58 | select { 59 | case ch <- vl: 60 | continue 61 | case <-stream.Context().Done(): 62 | break 63 | } 64 | } 65 | }() 66 | 67 | return ch, nil 68 | } 69 | 70 | // Write maps its arguments to a PutValuesRequest and calls PutValues. 71 | func (c *client) Write(ctx context.Context, vl *api.ValueList) error { 72 | pbVL, err := MarshalValueList(vl) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | stream, err := c.PutValues(ctx) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | req := &pb.PutValuesRequest{ 83 | ValueList: pbVL, 84 | } 85 | if err := stream.Send(req); err != nil { 86 | stream.CloseSend() 87 | return err 88 | } 89 | 90 | _, err = stream.CloseAndRecv() 91 | return err 92 | } 93 | -------------------------------------------------------------------------------- /exec/exec_test.go: -------------------------------------------------------------------------------- 1 | package exec // import "collectd.org/exec" 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "collectd.org/api" 10 | ) 11 | 12 | func TestSanitizeInterval(t *testing.T) { 13 | cases := []struct { 14 | arg time.Duration 15 | env string 16 | want time.Duration 17 | }{ 18 | {42 * time.Second, "", 42 * time.Second}, 19 | {42 * time.Second, "23", 42 * time.Second}, 20 | {0, "23", 23 * time.Second}, 21 | {0, "8.15", 8150 * time.Millisecond}, 22 | {0, "", 10 * time.Second}, 23 | {0, "--- INVALID ---", 10 * time.Second}, 24 | } 25 | 26 | for _, tc := range cases { 27 | if tc.env != "" { 28 | if err := os.Setenv("COLLECTD_INTERVAL", tc.env); err != nil { 29 | t.Fatal(err) 30 | } 31 | } else { // tc.env == "" 32 | if err := os.Unsetenv("COLLECTD_INTERVAL"); err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | 37 | got := sanitizeInterval(tc.arg) 38 | if got != tc.want { 39 | t.Errorf("COLLECTD_INTERVAL=%q sanitizeInterval(%v) = %v, want %v", tc.env, tc.arg, got, tc.want) 40 | } 41 | } 42 | } 43 | 44 | func Example() { 45 | e := NewExecutor() 46 | 47 | // simple "value" callback 48 | answer := func() api.Value { 49 | return api.Gauge(42) 50 | } 51 | e.ValueCallback(answer, &api.ValueList{ 52 | Identifier: api.Identifier{ 53 | Host: "example.com", 54 | Plugin: "golang", 55 | Type: "answer", 56 | TypeInstance: "live_universe_and_everything", 57 | }, 58 | Interval: time.Second, 59 | }) 60 | 61 | // "complex" void callback 62 | bicycles := func(ctx context.Context, interval time.Duration) { 63 | vl := &api.ValueList{ 64 | Identifier: api.Identifier{ 65 | Host: "example.com", 66 | Plugin: "golang", 67 | Type: "bicycles", 68 | }, 69 | Interval: interval, 70 | Time: time.Now(), 71 | Values: make([]api.Value, 1), 72 | } 73 | 74 | data := []struct { 75 | TypeInstance string 76 | Value api.Gauge 77 | }{ 78 | {"beijing", api.Gauge(9000000)}, 79 | } 80 | for _, d := range data { 81 | vl.Values[0] = d.Value 82 | vl.Identifier.TypeInstance = d.TypeInstance 83 | Putval.Write(ctx, vl) 84 | } 85 | } 86 | e.VoidCallback(bicycles, time.Second) 87 | 88 | // blocks forever 89 | e.Run(context.Background()) 90 | } 91 | -------------------------------------------------------------------------------- /format/putval.go: -------------------------------------------------------------------------------- 1 | // Package format provides utilities to format metrics and notifications in 2 | // various formats. 3 | package format // import "collectd.org/format" 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | "log" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "collectd.org/api" 15 | "collectd.org/meta" 16 | ) 17 | 18 | // Putval implements the Writer interface for PUTVAL formatted output. 19 | type Putval struct { 20 | w io.Writer 21 | } 22 | 23 | // NewPutval returns a new Putval object writing to the provided io.Writer. 24 | func NewPutval(w io.Writer) *Putval { 25 | return &Putval{ 26 | w: w, 27 | } 28 | } 29 | 30 | // Write formats the ValueList in the PUTVAL format and writes it to the 31 | // assiciated io.Writer. 32 | func (p *Putval) Write(_ context.Context, vl *api.ValueList) error { 33 | s, err := formatValues(vl) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | _, err = fmt.Fprintf(p.w, "PUTVAL %q interval=%.3f %s%s\n", 39 | vl.Identifier.String(), vl.Interval.Seconds(), formatMeta(vl.Meta), s) 40 | return err 41 | } 42 | 43 | func formatValues(vl *api.ValueList) (string, error) { 44 | fields := make([]string, 1+len(vl.Values)) 45 | 46 | fields[0] = formatTime(vl.Time) 47 | 48 | for i, v := range vl.Values { 49 | switch v := v.(type) { 50 | case api.Counter: 51 | fields[i+1] = fmt.Sprintf("%d", v) 52 | case api.Gauge: 53 | fields[i+1] = fmt.Sprintf("%.15g", v) 54 | case api.Derive: 55 | fields[i+1] = fmt.Sprintf("%d", v) 56 | default: 57 | return "", fmt.Errorf("unexpected type %T", v) 58 | } 59 | } 60 | 61 | return strings.Join(fields, ":"), nil 62 | } 63 | 64 | func formatTime(t time.Time) string { 65 | if t.IsZero() { 66 | return "N" 67 | } 68 | 69 | return fmt.Sprintf("%.3f", float64(t.UnixNano())/1000000000.0) 70 | } 71 | 72 | var stringWarning sync.Once 73 | 74 | func formatMeta(m meta.Data) string { 75 | if len(m) == 0 { 76 | return "" 77 | } 78 | 79 | var values []string 80 | for k, v := range m { 81 | // collectd only supports string meta data values as of 5.11. 82 | if !v.IsString() { 83 | stringWarning.Do(func() { 84 | log.Printf("Non-string metadata not supported yet") 85 | }) 86 | continue 87 | } 88 | values = append(values, fmt.Sprintf("meta:%s=%q ", k, v.String())) 89 | } 90 | 91 | return strings.Join(values, "") 92 | } 93 | -------------------------------------------------------------------------------- /network/client.go: -------------------------------------------------------------------------------- 1 | package network // import "collectd.org/network" 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | 8 | "collectd.org/api" 9 | ) 10 | 11 | // ClientOptions holds configuration options for Client. 12 | type ClientOptions struct { 13 | // SecurityLevel determines whether data is signed, encrypted or sent 14 | // in plain text. 15 | SecurityLevel SecurityLevel 16 | // Username and password for the "Sign" and "Encrypt" security levels. 17 | Username, Password string 18 | // Size of the send buffer. When zero, DefaultBufferSize is used. 19 | BufferSize int 20 | } 21 | 22 | // Client is a connection to a collectd server. It implements the 23 | // api.Writer interface. 24 | type Client struct { 25 | udp net.Conn 26 | buffer *Buffer 27 | opts ClientOptions 28 | } 29 | 30 | // Dial connects to the collectd server at address. "address" must be a network 31 | // address accepted by net.Dial(). 32 | func Dial(address string, opts ClientOptions) (*Client, error) { 33 | c, err := net.Dial("udp", address) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | b := NewBuffer(opts.BufferSize) 39 | if opts.SecurityLevel == Sign { 40 | b.Sign(opts.Username, opts.Password) 41 | } else if opts.SecurityLevel == Encrypt { 42 | b.Encrypt(opts.Username, opts.Password) 43 | } 44 | 45 | return &Client{ 46 | udp: c, 47 | buffer: b, 48 | opts: opts, 49 | }, nil 50 | } 51 | 52 | // Write adds a ValueList to the internal buffer. Data is only written to 53 | // the network when the buffer is full. 54 | func (c *Client) Write(ctx context.Context, vl *api.ValueList) error { 55 | if err := c.buffer.Write(ctx, vl); !errors.Is(err, ErrNotEnoughSpace) { 56 | return err 57 | } 58 | 59 | if err := c.Flush(); err != nil { 60 | return err 61 | } 62 | 63 | return c.buffer.Write(ctx, vl) 64 | } 65 | 66 | // Flush writes the contents of the underlying buffer to the network 67 | // immediately. 68 | func (c *Client) Flush() error { 69 | _, err := c.buffer.WriteTo(c.udp) 70 | return err 71 | } 72 | 73 | // Close writes remaining data to the network and closes the socket. You must 74 | // not use "c" after this call. 75 | func (c *Client) Close() error { 76 | if err := c.Flush(); err != nil { 77 | return err 78 | } 79 | 80 | if err := c.udp.Close(); err != nil { 81 | return err 82 | } 83 | 84 | c.buffer = nil 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 3 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 4 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 5 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 7 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 10 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 11 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 12 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 13 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 14 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 15 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 16 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 17 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 18 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 19 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= 20 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= 21 | google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= 22 | google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= 23 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 24 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 25 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 26 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | -------------------------------------------------------------------------------- /api/json_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | "time" 11 | 12 | "collectd.org/meta" 13 | "github.com/google/go-cmp/cmp" 14 | ) 15 | 16 | func TestValueList(t *testing.T) { 17 | vlWant := ValueList{ 18 | Identifier: Identifier{ 19 | Host: "example.com", 20 | Plugin: "golang", 21 | Type: "gauge", 22 | }, 23 | Time: time.Unix(1426585562, 999000000), 24 | Interval: 10 * time.Second, 25 | Values: []Value{Gauge(42)}, 26 | DSNames: []string{"legacy"}, 27 | Meta: meta.Data{ 28 | "foo": meta.String("bar"), 29 | }, 30 | } 31 | 32 | want := `{"values":[42],"dstypes":["gauge"],"dsnames":["legacy"],"time":1426585562.999,"interval":10.000,"host":"example.com","plugin":"golang","type":"gauge","meta":{"foo":"bar"}}` 33 | 34 | got, err := vlWant.MarshalJSON() 35 | if err != nil { 36 | t.Fatalf("ValueList.MarshalJSON() = %v", err) 37 | } 38 | if diff := cmp.Diff(want, string(got)); diff != "" { 39 | t.Errorf("ValueList.MarshalJSON() differs (+got/-want):\n%s", diff) 40 | } 41 | 42 | var vlGot ValueList 43 | if err := vlGot.UnmarshalJSON([]byte(want)); err != nil { 44 | t.Errorf("got %v, want nil)", err) 45 | } 46 | 47 | // Conversion to float64 and back takes its toll -- the conversion is 48 | // very accurate, but not bit-perfect. 49 | vlGot.Time = vlGot.Time.Round(time.Millisecond) 50 | if !reflect.DeepEqual(vlWant, vlGot) { 51 | t.Errorf("got %#v, want %#v)", vlGot, vlWant) 52 | } 53 | } 54 | 55 | func ExampleValueList_UnmarshalJSON() { 56 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 57 | data, err := ioutil.ReadAll(r.Body) 58 | if err != nil { 59 | log.Printf("while reading body: %v", err) 60 | http.Error(w, err.Error(), http.StatusInternalServerError) 61 | return 62 | } 63 | 64 | var vls []*ValueList 65 | if err := json.Unmarshal(data, &vls); err != nil { 66 | log.Printf("while parsing JSON: %v", err) 67 | http.Error(w, err.Error(), http.StatusBadRequest) 68 | return 69 | } 70 | 71 | for _, vl := range vls { 72 | var w Writer 73 | w.Write(r.Context(), vl) 74 | // "w" is a placeholder to avoid cyclic dependencies. 75 | // In real live, you'd do something like this here: 76 | // exec.Putval.Write(vl) 77 | } 78 | 79 | w.WriteHeader(http.StatusNoContent) 80 | }) 81 | 82 | log.Fatal(http.ListenAndServe(":8080", nil)) 83 | } 84 | -------------------------------------------------------------------------------- /plugin/fake/write.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 4 | // #cgo LDFLAGS: -ldl 5 | // #include 6 | // #include 7 | // #include "plugin.h" 8 | // 9 | // typedef struct { 10 | // const char *name; 11 | // plugin_write_cb callback; 12 | // user_data_t user_data; 13 | // } write_callback_t; 14 | // static write_callback_t *write_callbacks = NULL; 15 | // static size_t write_callbacks_num = 0; 16 | // 17 | // int plugin_register_write(const char *name, plugin_write_cb callback, 18 | // user_data_t const *user_data) { 19 | // write_callback_t *ptr = realloc( 20 | // write_callbacks, (write_callbacks_num + 1) * sizeof(*write_callbacks)); 21 | // if (ptr == NULL) { 22 | // return ENOMEM; 23 | // } 24 | // write_callbacks = ptr; 25 | // write_callbacks[write_callbacks_num] = (write_callback_t){ 26 | // .name = name, 27 | // .callback = callback, 28 | // .user_data = *user_data, 29 | // }; 30 | // write_callbacks_num++; 31 | // 32 | // return 0; 33 | // } 34 | // 35 | // int plugin_dispatch_values(value_list_t const *vl) { 36 | // data_set_t *ds = &(data_set_t){ 37 | // .ds_num = 1, 38 | // .ds = 39 | // &(data_source_t){ 40 | // .name = "value", 41 | // .min = 0, 42 | // .max = NAN, 43 | // }, 44 | // }; 45 | // 46 | // if (strcmp("derive", vl->type) == 0) { 47 | // strncpy(ds->type, vl->type, sizeof(ds->type)); 48 | // ds->ds[0].type = DS_TYPE_DERIVE; 49 | // } else if (strcmp("gauge", vl->type) == 0) { 50 | // strncpy(ds->type, vl->type, sizeof(ds->type)); 51 | // ds->ds[0].type = DS_TYPE_GAUGE; 52 | // } else if (strcmp("counter", vl->type) == 0) { 53 | // strncpy(ds->type, vl->type, sizeof(ds->type)); 54 | // ds->ds[0].type = DS_TYPE_COUNTER; 55 | // } else { 56 | // errno = EINVAL; 57 | // return errno; 58 | // } 59 | // 60 | // int ret = 0; 61 | // for (size_t i = 0; i < write_callbacks_num; i++) { 62 | // int err = 63 | // write_callbacks[i].callback(ds, vl, &write_callbacks[i].user_data); 64 | // if (err != 0) { 65 | // ret = err; 66 | // } 67 | // } 68 | // 69 | // return ret; 70 | // } 71 | // 72 | // void reset_write(void) { 73 | // for (size_t i = 0; i < write_callbacks_num; i++) { 74 | // user_data_t *ud = &write_callbacks[i].user_data; 75 | // if (ud->free_func == NULL) { 76 | // continue; 77 | // } 78 | // ud->free_func(ud->data); 79 | // ud->data = NULL; 80 | // } 81 | // free(write_callbacks); 82 | // write_callbacks = NULL; 83 | // write_callbacks_num = 0; 84 | // } 85 | import "C" 86 | -------------------------------------------------------------------------------- /format/graphite.go: -------------------------------------------------------------------------------- 1 | package format // import "collectd.org/format" 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "time" 9 | 10 | "collectd.org/api" 11 | ) 12 | 13 | // Graphite implements the Writer interface and writes ValueLists in Graphite 14 | // format to W. 15 | type Graphite struct { 16 | W io.Writer 17 | Prefix, Suffix string 18 | EscapeChar string 19 | SeparateInstances bool 20 | AlwaysAppendDS bool // TODO(octo): Implement support. 21 | replacer *strings.Replacer 22 | } 23 | 24 | func (g *Graphite) escape(in string) string { 25 | if g.replacer == nil { 26 | g.replacer = strings.NewReplacer( 27 | ".", g.EscapeChar, 28 | "\t", g.EscapeChar, 29 | "\"", g.EscapeChar, 30 | "\\", g.EscapeChar, 31 | ":", g.EscapeChar, 32 | "!", g.EscapeChar, 33 | "/", g.EscapeChar, 34 | "(", g.EscapeChar, 35 | ")", g.EscapeChar, 36 | "\n", g.EscapeChar, 37 | "\r", g.EscapeChar) 38 | } 39 | return g.replacer.Replace(in) 40 | } 41 | 42 | func (g *Graphite) formatName(id api.Identifier, dsName string) string { 43 | var instanceSeparator = "-" 44 | if g.SeparateInstances { 45 | instanceSeparator = "." 46 | } 47 | 48 | host := g.escape(id.Host) 49 | plugin := g.escape(id.Plugin) 50 | if id.PluginInstance != "" { 51 | plugin += instanceSeparator + g.escape(id.PluginInstance) 52 | } 53 | 54 | typ := id.Type 55 | if id.TypeInstance != "" { 56 | typ += instanceSeparator + g.escape(id.TypeInstance) 57 | } 58 | 59 | name := g.Prefix + host + g.Suffix + "." + plugin + "." + typ 60 | if dsName != "" { 61 | name += "." + g.escape(dsName) 62 | } 63 | 64 | return name 65 | } 66 | 67 | func (g *Graphite) formatValue(v api.Value) (string, error) { 68 | switch v := v.(type) { 69 | case api.Gauge: 70 | return fmt.Sprintf("%.15g", v), nil 71 | case api.Derive, api.Counter: 72 | return fmt.Sprintf("%v", v), nil 73 | default: 74 | return "", fmt.Errorf("unexpected type %T", v) 75 | } 76 | } 77 | 78 | // Write formats the ValueList in the PUTVAL format and writes it to the 79 | // assiciated io.Writer. 80 | func (g *Graphite) Write(_ context.Context, vl *api.ValueList) error { 81 | for i, v := range vl.Values { 82 | dsName := "" 83 | if g.AlwaysAppendDS || len(vl.Values) != 1 { 84 | dsName = vl.DSName(i) 85 | } 86 | 87 | name := g.formatName(vl.Identifier, dsName) 88 | 89 | val, err := g.formatValue(v) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | t := vl.Time 95 | if t.IsZero() { 96 | t = time.Now() 97 | } 98 | 99 | fmt.Fprintf(g.W, "%s %s %d\r\n", name, val, t.Unix()) 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /plugin/c.go: -------------------------------------------------------------------------------- 1 | // +build go1.5,cgo 2 | 3 | package plugin // import "collectd.org/plugin" 4 | 5 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 6 | // #cgo LDFLAGS: -ldl 7 | // #include "plugin.h" 8 | // #include 9 | // #include 10 | // 11 | // void value_list_add (value_list_t *vl, value_t v) { 12 | // value_t *tmp = realloc (vl->values, sizeof(v) * (vl->values_len + 1)); 13 | // if (tmp == NULL) { 14 | // errno = ENOMEM; 15 | // return; 16 | // } 17 | // vl->values = tmp; 18 | // vl->values[vl->values_len] = v; 19 | // vl->values_len++; 20 | // } 21 | // 22 | // data_source_t *ds_dsrc(data_set_t const *ds, size_t i) { return ds->ds + i; } 23 | // 24 | // void value_list_add_counter (value_list_t *vl, counter_t c) { 25 | // value_list_add (vl, (value_t){ 26 | // .counter = c, 27 | // }); 28 | // } 29 | // 30 | // void value_list_add_gauge (value_list_t *vl, gauge_t g) { 31 | // value_list_add (vl, (value_t){ 32 | // .gauge = g, 33 | // }); 34 | // } 35 | // 36 | // void value_list_add_derive (value_list_t *vl, derive_t d) { 37 | // value_list_add (vl, (value_t){ 38 | // .derive = d, 39 | // }); 40 | // } 41 | // 42 | // counter_t value_list_get_counter (value_list_t *vl, size_t i) { 43 | // return vl->values[i].counter; 44 | // } 45 | // 46 | // gauge_t value_list_get_gauge (value_list_t *vl, size_t i) { 47 | // return vl->values[i].gauge; 48 | // } 49 | // 50 | // derive_t value_list_get_derive (value_list_t *vl, size_t i) { 51 | // return vl->values[i].derive; 52 | // } 53 | // 54 | // static int *timeout_ptr; 55 | // int timeout_wrapper(void) { 56 | // if (timeout_ptr == NULL) { 57 | // void *hnd = dlopen(NULL, RTLD_LAZY); 58 | // timeout_ptr = dlsym(hnd, "timeout_g"); 59 | // dlclose(hnd); 60 | // } 61 | // return *timeout_ptr; 62 | // } 63 | // 64 | // typedef int (*plugin_complex_config_cb)(oconfig_item_t *); 65 | // 66 | // static int (*register_complex_config_ptr) (const char *, plugin_complex_config_cb); 67 | // int register_complex_config_wrapper (const char *name, plugin_complex_config_cb callback) { 68 | // if (register_complex_config_ptr == NULL) { 69 | // void *hnd = dlopen(NULL, RTLD_LAZY); 70 | // register_complex_config_ptr = dlsym(hnd, "plugin_register_complex_config"); 71 | // dlclose(hnd); 72 | // } 73 | // return (*register_complex_config_ptr) (name, callback); 74 | // } 75 | // 76 | // static int (*register_init_ptr) (const char *, plugin_init_cb); 77 | // int register_init_wrapper (const char *name, plugin_init_cb callback) { 78 | // if (register_init_ptr == NULL) { 79 | // void *hnd = dlopen(NULL, RTLD_LAZY); 80 | // register_init_ptr = dlsym(hnd, "plugin_register_init"); 81 | // dlclose(hnd); 82 | // } 83 | // return (*register_init_ptr) (name, callback); 84 | // } 85 | import "C" 86 | -------------------------------------------------------------------------------- /cdtime/cdtime.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package cdtime implements methods to convert from and to collectd's internal time 3 | representation, cdtime_t. 4 | */ 5 | package cdtime // import "collectd.org/cdtime" 6 | 7 | import ( 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | // Time represens a time in collectd's internal representation. 13 | type Time uint64 14 | 15 | // New returns a new Time representing time t. 16 | func New(t time.Time) Time { 17 | if t.IsZero() { 18 | return 0 19 | } 20 | return newNano(uint64(t.UnixNano())) 21 | } 22 | 23 | // NewDuration returns a new Time representing duration d. 24 | func NewDuration(d time.Duration) Time { 25 | return newNano(uint64(d.Nanoseconds())) 26 | } 27 | 28 | // Time converts and returns the time as time.Time. 29 | func (t Time) Time() time.Time { 30 | if t == 0 { 31 | return time.Time{} 32 | } 33 | 34 | s, ns := t.decompose() 35 | return time.Unix(s, ns) 36 | } 37 | 38 | // Duration converts and returns the duration as time.Duration. 39 | func (t Time) Duration() time.Duration { 40 | s, ns := t.decompose() 41 | return time.Duration(1000000000*s+ns) * time.Nanosecond 42 | } 43 | 44 | // String returns the string representation of Time. The format used is seconds 45 | // since the epoch with millisecond precision, e.g. "1426588900.328". 46 | func (t Time) String() string { 47 | f := t.Float() 48 | return strconv.FormatFloat(f /* format */, 'f' /* precision */, 3 /* bits */, 64) 49 | } 50 | 51 | // Float returns the time as seocnds since epoch. This is a lossy conversion, 52 | // which will lose up to 11 bits. This means that the returned value should be 53 | // considered to have roughly microsecond precision. 54 | func (t Time) Float() float64 { 55 | s, ns := t.decompose() 56 | return float64(s) + float64(ns)/1000000000.0 57 | } 58 | 59 | // MarshalJSON implements the "encoding/json".Marshaler interface for Time. 60 | func (t Time) MarshalJSON() ([]byte, error) { 61 | return []byte(t.String()), nil 62 | } 63 | 64 | // UnmarshalJSON implements the "encoding/json".Unmarshaler interface for Time. 65 | func (t *Time) UnmarshalJSON(data []byte) error { 66 | f, err := strconv.ParseFloat(string(data) /* bits */, 64) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | s := uint64(f) 72 | ns := uint64((f - float64(s)) * 1000000000.0) 73 | 74 | *t = newNano(1000000000*s + ns) 75 | return nil 76 | } 77 | 78 | func (t Time) decompose() (s, ns int64) { 79 | s = int64(t >> 30) 80 | 81 | ns = (int64(t&0x3fffffff) * 1000000000) 82 | // add 2^29 to correct rounding behavior. 83 | ns = (ns + (1 << 29)) >> 30 84 | 85 | return 86 | } 87 | 88 | func newNano(ns uint64) Time { 89 | // break into seconds and nano-seconds so the left-shift doesn't overflow. 90 | s := (ns / 1000000000) << 30 91 | 92 | ns = (ns % 1000000000) << 30 93 | // add 5e8 to correct rounding behavior. 94 | ns = (ns + 500000000) / 1000000000 95 | 96 | return Time(s | ns) 97 | } 98 | -------------------------------------------------------------------------------- /format/graphite_test.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | "time" 8 | 9 | "collectd.org/api" 10 | ) 11 | 12 | func TestWrite(t *testing.T) { 13 | ctx := context.Background() 14 | 15 | cases := []struct { 16 | ValueList *api.ValueList 17 | Graphite *Graphite 18 | Want string 19 | }{ 20 | { // case 0 21 | ValueList: &api.ValueList{ 22 | Identifier: api.Identifier{ 23 | Host: "example.com", 24 | Plugin: "golang", 25 | PluginInstance: "example", 26 | Type: "gauge", 27 | TypeInstance: "answer", 28 | }, 29 | Time: time.Unix(1426975989, 1), 30 | Interval: 10 * time.Second, 31 | Values: []api.Value{api.Gauge(42)}, 32 | }, 33 | Graphite: &Graphite{ 34 | Prefix: "-->", 35 | Suffix: "<--", 36 | EscapeChar: "_", 37 | SeparateInstances: false, 38 | AlwaysAppendDS: true, 39 | }, 40 | Want: "-->example_com<--.golang-example.gauge-answer.value 42 1426975989\r\n", 41 | }, 42 | { // case 1 43 | ValueList: &api.ValueList{ 44 | Identifier: api.Identifier{ 45 | Host: "example.com", 46 | Plugin: "golang", 47 | PluginInstance: "example", 48 | Type: "gauge", 49 | TypeInstance: "answer", 50 | }, 51 | Time: time.Unix(1426975989, 1), 52 | Interval: 10 * time.Second, 53 | Values: []api.Value{api.Derive(1337)}, 54 | }, 55 | Graphite: &Graphite{ 56 | Prefix: "collectd.", 57 | Suffix: "", 58 | EscapeChar: "@", 59 | SeparateInstances: true, 60 | AlwaysAppendDS: false, 61 | }, 62 | Want: "collectd.example@com.golang.example.gauge.answer 1337 1426975989\r\n", 63 | }, 64 | { // case 2 65 | ValueList: &api.ValueList{ 66 | Identifier: api.Identifier{ 67 | Host: "example.com", 68 | Plugin: "golang", 69 | Type: "gauge", 70 | }, 71 | Time: time.Unix(1426975989, 1), 72 | Interval: 10 * time.Second, 73 | Values: []api.Value{api.Gauge(42), api.Derive(1337)}, 74 | }, 75 | Graphite: &Graphite{ 76 | Prefix: "collectd.", 77 | Suffix: "", 78 | EscapeChar: "_", 79 | SeparateInstances: true, 80 | AlwaysAppendDS: false, 81 | }, 82 | Want: "collectd.example_com.golang.gauge.0 42 1426975989\r\n" + 83 | "collectd.example_com.golang.gauge.1 1337 1426975989\r\n", 84 | }, 85 | } 86 | 87 | for i, c := range cases { 88 | buf := &bytes.Buffer{} 89 | c.Graphite.W = buf 90 | 91 | if err := c.Graphite.Write(ctx, c.ValueList); err != nil { 92 | t.Errorf("case %d: got %v, want %v", i, err, nil) 93 | } 94 | 95 | got := buf.String() 96 | if got != c.Want { 97 | t.Errorf("got %q, want %q", got, c.Want) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /network/network_x_test.go: -------------------------------------------------------------------------------- 1 | package network_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "testing" 9 | "time" 10 | 11 | "collectd.org/api" 12 | "collectd.org/network" 13 | "github.com/google/go-cmp/cmp" 14 | "github.com/google/go-cmp/cmp/cmpopts" 15 | "golang.org/x/net/nettest" 16 | ) 17 | 18 | type testPasswordLookup map[string]string 19 | 20 | func (l testPasswordLookup) Password(user string) (string, error) { 21 | pw, ok := l[user] 22 | if !ok { 23 | return "", fmt.Errorf("user %q not found", user) 24 | } 25 | return pw, nil 26 | } 27 | 28 | func TestNetwork(t *testing.T) { 29 | ctx, cancel := context.WithCancel(context.Background()) 30 | defer cancel() 31 | 32 | const ( 33 | username = "TestNetwork" 34 | password = `oi5aGh7oLo0mai5oaG8zei8a` 35 | ) 36 | 37 | conn, err := nettest.NewLocalPacketListener("udp") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | defer conn.Close() 42 | 43 | ch := make(chan *api.ValueList) 44 | go func() { 45 | srv := &network.Server{ 46 | Conn: conn.(*net.UDPConn), 47 | Writer: api.WriterFunc(func(_ context.Context, vl *api.ValueList) error { 48 | ch <- vl 49 | return nil 50 | }), 51 | PasswordLookup: testPasswordLookup{ 52 | username: password, 53 | }, 54 | } 55 | 56 | err := srv.ListenAndWrite(ctx) 57 | if !errors.Is(err, context.Canceled) { 58 | t.Errorf("Server.ListenAndWrite() = %v, want %v", err, context.Canceled) 59 | } 60 | close(ch) 61 | }() 62 | 63 | var want []*api.ValueList 64 | go func() { 65 | client, err := network.Dial(conn.LocalAddr().String(), 66 | network.ClientOptions{ 67 | SecurityLevel: network.Encrypt, 68 | Username: username, 69 | Password: password, 70 | }) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | vl := &api.ValueList{ 76 | Identifier: api.Identifier{ 77 | Host: "example.com", 78 | Plugin: "TestNetwork", 79 | Type: "gauge", 80 | }, 81 | Time: time.Unix(1588164686, 0), 82 | Interval: 10 * time.Second, 83 | Values: []api.Value{api.Gauge(42)}, 84 | } 85 | 86 | for i := 0; i < 30; i++ { 87 | if err := client.Write(ctx, vl); err != nil { 88 | t.Errorf("client.Write() = %v", err) 89 | break 90 | } 91 | want = append(want, vl.Clone()) 92 | 93 | vl.Time = vl.Time.Add(vl.Interval) 94 | } 95 | 96 | if err := client.Close(); err != nil { 97 | t.Errorf("client.Close() = %v", err) 98 | } 99 | }() 100 | 101 | var got []*api.ValueList 102 | loop: 103 | for { 104 | select { 105 | case vl, ok := <-ch: 106 | if !ok { 107 | break loop 108 | } 109 | got = append(got, vl) 110 | case <-time.After(100 * time.Millisecond): 111 | // cancel the context so the server returns. 112 | cancel() 113 | } 114 | } 115 | 116 | if diff := cmp.Diff(want, got, cmpopts.EquateEmpty()); diff != "" { 117 | t.Errorf("sent and received value lists differ (+got/-want):\n%s", diff) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /network/crypto_test.go: -------------------------------------------------------------------------------- 1 | package network // import "collectd.org/network" 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | type mockPasswordLookup map[string]string 11 | 12 | func (l mockPasswordLookup) Password(user string) (string, error) { 13 | pass, ok := l[user] 14 | if !ok { 15 | return "", errors.New("not found") 16 | } 17 | 18 | return pass, nil 19 | } 20 | 21 | func TestSign(t *testing.T) { 22 | want := []byte{ 23 | 2, 0, 0, 41, 24 | 0xcd, 0xa5, 0x9a, 0x37, 0xb0, 0x81, 0xc2, 0x31, 25 | 0x24, 0x2a, 0x6d, 0xbd, 0xfb, 0x44, 0xdb, 0xd7, 26 | 0x41, 0x2a, 0xf4, 0x29, 0x83, 0xde, 0xa5, 0x11, 27 | 0x96, 0xd2, 0xe9, 0x30, 0x21, 0xae, 0xc5, 0x45, 28 | 'a', 'd', 'm', 'i', 'n', 29 | 'c', 'o', 'l', 'l', 'e', 'c', 't', 'd', 30 | } 31 | got := signSHA256([]byte{'c', 'o', 'l', 'l', 'e', 'c', 't', 'd'}, "admin", "admin") 32 | 33 | if !reflect.DeepEqual(got, want) { 34 | t.Errorf("got %v, want %v", got, want) 35 | } 36 | 37 | passwords := mockPasswordLookup{ 38 | "admin": "admin", 39 | } 40 | ok, err := verifySHA256(want[4:41], want[41:], passwords) 41 | if !ok || err != nil { 42 | t.Errorf("got (%v, %v), want (true, nil)", ok, err) 43 | } 44 | 45 | want[41], want[42] = want[42], want[41] // corrupt data 46 | ok, err = verifySHA256(want[4:41], want[41:], passwords) 47 | if ok || err != nil { 48 | t.Errorf("got (%v, %v), want (false, nil)", ok, err) 49 | } 50 | 51 | want[41], want[42] = want[42], want[41] // fix data 52 | passwords["admin"] = "test123" // different password 53 | ok, err = verifySHA256(want[4:41], want[41:], passwords) 54 | if ok || err != nil { 55 | t.Errorf("got (%v, %v), want (false, nil)", ok, err) 56 | } 57 | } 58 | 59 | func TestEncrypt(t *testing.T) { 60 | plaintext := []byte{'c', 'o', 'l', 'l', 'e', 'c', 't', 'd'} 61 | // actual ciphertext depends on IV -- only check the first part 62 | want := []byte{ 63 | 0x02, 0x10, // part type 64 | 0x00, 0x37, // part length 65 | 0x00, 0x05, // username length 66 | 0x61, 0x64, 0x6d, 0x69, 0x6e, // username 67 | // IV 68 | // SHA1 69 | // encrypted data 70 | } 71 | 72 | ciphertext, err := encryptAES256(plaintext, "admin", "admin") 73 | if !bytes.Equal(want, ciphertext[:11]) || err != nil { 74 | t.Errorf("got (%v, %v), want (%v, nil)", ciphertext[:11], err, want) 75 | } 76 | 77 | passwords := mockPasswordLookup{ 78 | "admin": "admin", 79 | } 80 | if got, err := decryptAES256(ciphertext[4:], passwords); !bytes.Equal(got, plaintext) || err != nil { 81 | t.Errorf("got (%v, %v), want (%v, nil)", got, err, plaintext) 82 | } 83 | 84 | ciphertext[47], ciphertext[48] = ciphertext[48], ciphertext[47] // corrupt data 85 | if got, err := decryptAES256(ciphertext[4:], passwords); got != nil || err == nil { 86 | t.Errorf("got (%v, %v), want (nil, \"checksum mismatch\")", got, err) 87 | } 88 | 89 | ciphertext[47], ciphertext[48] = ciphertext[48], ciphertext[47] // fix data 90 | passwords["admin"] = "test123" // different password 91 | if got, err := decryptAES256(ciphertext[4:], passwords); got != nil || err == nil { 92 | t.Errorf("got (%v, %v), want (nil, \"no such user\")", got, err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /cdtime/cdtime_test.go: -------------------------------------------------------------------------------- 1 | package cdtime_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "collectd.org/cdtime" 9 | ) 10 | 11 | // TestNew converts a time.Time to a cdtime.Time and back, expecting the 12 | // original time.Time back. 13 | func TestNew(t *testing.T) { 14 | cases := []string{ 15 | "2009-02-04T21:00:57-08:00", 16 | "2009-02-04T21:00:57.1-08:00", 17 | "2009-02-04T21:00:57.01-08:00", 18 | "2009-02-04T21:00:57.001-08:00", 19 | "2009-02-04T21:00:57.0001-08:00", 20 | "2009-02-04T21:00:57.00001-08:00", 21 | "2009-02-04T21:00:57.000001-08:00", 22 | "2009-02-04T21:00:57.0000001-08:00", 23 | "2009-02-04T21:00:57.00000001-08:00", 24 | "2009-02-04T21:00:57.000000001-08:00", 25 | } 26 | 27 | for _, s := range cases { 28 | want, err := time.Parse(time.RFC3339Nano, s) 29 | if err != nil { 30 | t.Errorf("time.Parse(%q): got (%v, %v), want (, nil)", s, want, err) 31 | continue 32 | } 33 | 34 | ct := cdtime.New(want) 35 | got := ct.Time() 36 | if !got.Equal(want) { 37 | t.Errorf("cdtime.Time(): got %v, want %v", got, want) 38 | } 39 | } 40 | } 41 | 42 | func TestNew_zero(t *testing.T) { 43 | var ( 44 | got = cdtime.New(time.Time{}) 45 | want = cdtime.Time(0) 46 | ) 47 | if got != want { 48 | t.Errorf("cdtime.New(time.Time{}) = %v, want %v", got, want) 49 | } 50 | 51 | if got := cdtime.Time(0).Time(); !got.IsZero() { 52 | t.Errorf("cdtime.Time(0).Time() = %v, want zero value (%v)", got, time.Time{}) 53 | } 54 | } 55 | 56 | func TestMarshalJSON(t *testing.T) { 57 | tm := time.Unix(1587671455, 499000000) 58 | 59 | orig := cdtime.New(tm) 60 | data, err := json.Marshal(orig) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | var got cdtime.Time 66 | if err := json.Unmarshal(data, &got); err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | // JSON Marshaling is not loss-less, because it only encodes 71 | // millisecond precision. 72 | if got, want := got.String(), "1587671455.499"; got != want { 73 | t.Errorf("json.Unmarshal() result differs: got %q, want %q", got, want) 74 | } 75 | } 76 | 77 | func TestNewDuration(t *testing.T) { 78 | cases := []struct { 79 | d time.Duration 80 | want cdtime.Time 81 | }{ 82 | // 1439981652801860766 * 2^30 / 10^9 = 1546168526406004689.4 83 | {1439981652801860766 * time.Nanosecond, cdtime.Time(1546168526406004689)}, 84 | // 1439981836985281914 * 2^30 / 10^9 = 1546168724171447263.4 85 | {1439981836985281914 * time.Nanosecond, cdtime.Time(1546168724171447263)}, 86 | // 1439981880053705608 * 2^30 / 10^9 = 1546168770415815077.4 87 | {1439981880053705608 * time.Nanosecond, cdtime.Time(1546168770415815077)}, 88 | // 1439981880053705920 * 2^30 / 10^9 = 1546168770415815412.5 89 | {1439981880053705920 * time.Nanosecond, cdtime.Time(1546168770415815413)}, 90 | {0, 0}, 91 | } 92 | 93 | for _, tc := range cases { 94 | d := cdtime.NewDuration(tc.d) 95 | if got, want := d, tc.want; got != want { 96 | t.Errorf("NewDuration(%v) = %d, want %d", tc.d, got, want) 97 | } 98 | 99 | if got, want := d.Duration(), tc.d; got != want { 100 | t.Errorf("%#v.Duration() = %v, want %v", d, got, want) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /format/putval_test.go: -------------------------------------------------------------------------------- 1 | package format_test 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "collectd.org/api" 10 | "collectd.org/format" 11 | "collectd.org/meta" 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestPutval(t *testing.T) { 16 | baseVL := api.ValueList{ 17 | Identifier: api.Identifier{ 18 | Host: "example.com", 19 | Plugin: "TestPutval", 20 | Type: "derive", 21 | }, 22 | Interval: 10 * time.Second, 23 | Values: []api.Value{api.Derive(42)}, 24 | DSNames: []string{"value"}, 25 | } 26 | 27 | cases := []struct { 28 | title string 29 | modify func(*api.ValueList) 30 | want string 31 | wantErr bool 32 | }{ 33 | { 34 | title: "derive", 35 | want: `PUTVAL "example.com/TestPutval/derive" interval=10.000 N:42` + "\n", 36 | }, 37 | { 38 | title: "gauge", 39 | modify: func(vl *api.ValueList) { 40 | vl.Type = "gauge" 41 | vl.Values = []api.Value{api.Gauge(20.0 / 3.0)} 42 | }, 43 | want: `PUTVAL "example.com/TestPutval/gauge" interval=10.000 N:6.66666666666667` + "\n", 44 | }, 45 | { 46 | title: "counter", 47 | modify: func(vl *api.ValueList) { 48 | vl.Type = "counter" 49 | vl.Values = []api.Value{api.Counter(31337)} 50 | }, 51 | want: `PUTVAL "example.com/TestPutval/counter" interval=10.000 N:31337` + "\n", 52 | }, 53 | { 54 | title: "multiple values", 55 | modify: func(vl *api.ValueList) { 56 | vl.Type = "if_octets" 57 | vl.Values = []api.Value{api.Derive(1), api.Derive(2)} 58 | vl.DSNames = []string{"rx", "tx"} 59 | }, 60 | want: `PUTVAL "example.com/TestPutval/if_octets" interval=10.000 N:1:2` + "\n", 61 | }, 62 | { 63 | title: "invalid type", 64 | modify: func(vl *api.ValueList) { 65 | vl.Values = []api.Value{nil} 66 | }, 67 | wantErr: true, 68 | }, 69 | { 70 | title: "time", 71 | modify: func(vl *api.ValueList) { 72 | vl.Time = time.Unix(1588087972, 987654321) 73 | }, 74 | want: `PUTVAL "example.com/TestPutval/derive" interval=10.000 1588087972.988:42` + "\n", 75 | }, 76 | { 77 | title: "interval", 78 | modify: func(vl *api.ValueList) { 79 | vl.Interval = 9876543 * time.Microsecond 80 | }, 81 | want: `PUTVAL "example.com/TestPutval/derive" interval=9.877 N:42` + "\n", 82 | }, 83 | { 84 | title: "meta_data", 85 | modify: func(vl *api.ValueList) { 86 | vl.Meta = meta.Data{ 87 | "key": meta.String("value"), 88 | "ignored": meta.Bool(true), 89 | } 90 | }, 91 | want: `PUTVAL "example.com/TestPutval/derive" interval=10.000 meta:key="value" N:42` + "\n", 92 | }, 93 | } 94 | 95 | for _, tc := range cases { 96 | t.Run(tc.title, func(t *testing.T) { 97 | ctx := context.Background() 98 | 99 | vl := baseVL 100 | if tc.modify != nil { 101 | tc.modify(&vl) 102 | } 103 | 104 | var b strings.Builder 105 | err := format.NewPutval(&b).Write(ctx, &vl) 106 | if gotErr := err != nil; gotErr != tc.wantErr { 107 | t.Errorf("Putval.Write(%#v) = %v, want error %v", &vl, err, tc.wantErr) 108 | } 109 | if tc.wantErr { 110 | return 111 | } 112 | 113 | if diff := cmp.Diff(tc.want, b.String()); diff != "" { 114 | t.Errorf("Putval.Write(%#v) differs (+got/-want):\n%s", &vl, diff) 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /rpc/marshalling.go: -------------------------------------------------------------------------------- 1 | package rpc // import "collectd.org/rpc" 2 | 3 | import ( 4 | "collectd.org/api" 5 | pb "collectd.org/rpc/proto/types" 6 | "github.com/golang/protobuf/ptypes" 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/codes" 9 | ) 10 | 11 | // MarshalValue converts an api.Value to a pb.Value. 12 | func MarshalValue(v api.Value) (*pb.Value, error) { 13 | switch v := v.(type) { 14 | case api.Counter: 15 | return &pb.Value{ 16 | Value: &pb.Value_Counter{Counter: uint64(v)}, 17 | }, nil 18 | case api.Derive: 19 | return &pb.Value{ 20 | Value: &pb.Value_Derive{Derive: int64(v)}, 21 | }, nil 22 | case api.Gauge: 23 | return &pb.Value{ 24 | Value: &pb.Value_Gauge{Gauge: float64(v)}, 25 | }, nil 26 | default: 27 | return nil, grpc.Errorf(codes.InvalidArgument, "%T values are not supported", v) 28 | } 29 | } 30 | 31 | // UnmarshalValue converts a pb.Value to an api.Value. 32 | func UnmarshalValue(in *pb.Value) (api.Value, error) { 33 | switch v := in.GetValue().(type) { 34 | case *pb.Value_Counter: 35 | return api.Counter(v.Counter), nil 36 | case *pb.Value_Derive: 37 | return api.Derive(v.Derive), nil 38 | case *pb.Value_Gauge: 39 | return api.Gauge(v.Gauge), nil 40 | default: 41 | return nil, grpc.Errorf(codes.InvalidArgument, "%T values are not supported", v) 42 | } 43 | } 44 | 45 | // MarshalIdentifier converts an api.Identifier to a pb.Identifier. 46 | func MarshalIdentifier(id *api.Identifier) *pb.Identifier { 47 | return &pb.Identifier{ 48 | Host: id.Host, 49 | Plugin: id.Plugin, 50 | PluginInstance: id.PluginInstance, 51 | Type: id.Type, 52 | TypeInstance: id.TypeInstance, 53 | } 54 | } 55 | 56 | // UnmarshalIdentifier converts a pb.Identifier to an api.Identifier. 57 | func UnmarshalIdentifier(in *pb.Identifier) *api.Identifier { 58 | return &api.Identifier{ 59 | Host: in.Host, 60 | Plugin: in.Plugin, 61 | PluginInstance: in.PluginInstance, 62 | Type: in.Type, 63 | TypeInstance: in.TypeInstance, 64 | } 65 | } 66 | 67 | // MarshalValueList converts an api.ValueList to a pb.ValueList. 68 | func MarshalValueList(vl *api.ValueList) (*pb.ValueList, error) { 69 | t, err := ptypes.TimestampProto(vl.Time) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | var pbValues []*pb.Value 75 | for _, v := range vl.Values { 76 | pbValue, err := MarshalValue(v) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | pbValues = append(pbValues, pbValue) 82 | } 83 | 84 | return &pb.ValueList{ 85 | Values: pbValues, 86 | Time: t, 87 | Interval: ptypes.DurationProto(vl.Interval), 88 | Identifier: MarshalIdentifier(&vl.Identifier), 89 | }, nil 90 | } 91 | 92 | // UnmarshalValueList converts a pb.ValueList to an api.ValueList. 93 | func UnmarshalValueList(in *pb.ValueList) (*api.ValueList, error) { 94 | t, err := ptypes.Timestamp(in.GetTime()) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | interval, err := ptypes.Duration(in.GetInterval()) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | var values []api.Value 105 | for _, pbValue := range in.GetValues() { 106 | v, err := UnmarshalValue(pbValue) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | values = append(values, v) 112 | } 113 | 114 | return &api.ValueList{ 115 | Identifier: *UnmarshalIdentifier(in.GetIdentifier()), 116 | Time: t, 117 | Interval: interval, 118 | Values: values, 119 | DSNames: in.DsNames, 120 | }, nil 121 | } 122 | -------------------------------------------------------------------------------- /plugin/fake/read.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 4 | // #cgo LDFLAGS: -ldl 5 | // #include 6 | // #include 7 | // #include "plugin.h" 8 | // 9 | // typedef struct { 10 | // char *group; 11 | // char *name; 12 | // plugin_read_cb callback; 13 | // cdtime_t interval; 14 | // user_data_t user_data; 15 | // } read_callback_t; 16 | // read_callback_t *read_callbacks = NULL; 17 | // size_t read_callbacks_num = 0; 18 | // 19 | // int plugin_register_complex_read(const char *group, const char *name, 20 | // plugin_read_cb callback, cdtime_t interval, 21 | // user_data_t const *user_data) { 22 | // if (interval == 0) { 23 | // interval = plugin_get_interval(); 24 | // } 25 | // 26 | // read_callback_t *ptr = realloc( 27 | // read_callbacks, (read_callbacks_num + 1) * sizeof(*read_callbacks)); 28 | // if (ptr == NULL) { 29 | // return ENOMEM; 30 | // } 31 | // read_callbacks = ptr; 32 | // read_callbacks[read_callbacks_num] = (read_callback_t){ 33 | // .group = (group != NULL) ? strdup(group) : NULL, 34 | // .name = strdup(name), 35 | // .callback = callback, 36 | // .interval = interval, 37 | // .user_data = *user_data, 38 | // }; 39 | // read_callbacks_num++; 40 | // 41 | // return 0; 42 | // } 43 | // 44 | // void plugin_set_interval(cdtime_t); 45 | // static int read_all(void) { 46 | // cdtime_t save_interval = plugin_get_interval(); 47 | // int ret = 0; 48 | // 49 | // for (size_t i = 0; i < read_callbacks_num; i++) { 50 | // read_callback_t *cb = read_callbacks + i; 51 | // plugin_set_interval(cb->interval); 52 | // int err = cb->callback(&cb->user_data); 53 | // if (err != 0) { 54 | // ret = err; 55 | // } 56 | // } 57 | // 58 | // plugin_set_interval(save_interval); 59 | // return ret; 60 | // } 61 | // 62 | // void reset_read(void) { 63 | // for (size_t i = 0; i < read_callbacks_num; i++) { 64 | // free(read_callbacks[i].name); 65 | // free(read_callbacks[i].group); 66 | // user_data_t *ud = &read_callbacks[i].user_data; 67 | // if (ud->free_func == NULL) { 68 | // continue; 69 | // } 70 | // ud->free_func(ud->data); 71 | // ud->data = NULL; 72 | // } 73 | // free(read_callbacks); 74 | // read_callbacks = NULL; 75 | // read_callbacks_num = 0; 76 | // } 77 | import "C" 78 | 79 | import ( 80 | "fmt" 81 | "unsafe" 82 | 83 | "collectd.org/cdtime" 84 | ) 85 | 86 | func ReadAll() error { 87 | status, err := C.read_all() 88 | if err != nil { 89 | return err 90 | } 91 | if status != 0 { 92 | return fmt.Errorf("read_all() = %d", status) 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // ReadCallback represents a data associated with a registered read callback. 99 | type ReadCallback struct { 100 | Group, Name string 101 | Interval cdtime.Time 102 | } 103 | 104 | // ReadCallbacks returns the data associated with all registered read 105 | // callbacks. 106 | func ReadCallbacks() []ReadCallback { 107 | var ret []ReadCallback 108 | 109 | for i := C.size_t(0); i < C.read_callbacks_num; i++ { 110 | // Go pointer arithmetic that does the equivalent of C's `read_callbacks[i]`. 111 | cb := (*C.read_callback_t)(unsafe.Pointer(uintptr(unsafe.Pointer(C.read_callbacks)) + uintptr(C.sizeof_read_callback_t*i))) 112 | ret = append(ret, ReadCallback{ 113 | Group: C.GoString(cb.group), 114 | Name: C.GoString(cb.name), 115 | Interval: cdtime.Time(cb.interval), 116 | }) 117 | } 118 | 119 | return ret 120 | } 121 | -------------------------------------------------------------------------------- /network/server.go: -------------------------------------------------------------------------------- 1 | package network // import "collectd.org/network" 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "sync" 8 | 9 | "collectd.org/api" 10 | ) 11 | 12 | // ListenAndWrite listens on the provided UDP address, parses the received 13 | // packets and writes them to the provided api.Writer. 14 | // This is a convenience function for a minimally configured server. If you 15 | // need more control, see the "Server" type below. 16 | func ListenAndWrite(ctx context.Context, address string, d api.Writer) error { 17 | srv := &Server{ 18 | Addr: address, 19 | Writer: d, 20 | } 21 | 22 | return srv.ListenAndWrite(ctx) 23 | } 24 | 25 | // Server holds parameters for running a collectd server. 26 | type Server struct { 27 | // UDP connection the server listens on. If Conn is nil, a new server 28 | // connection is opened. The connection is closed by ListenAndWrite 29 | // before returning. 30 | Conn *net.UDPConn 31 | // Address to listen on if Conn is nil. If Addr is empty, too, then the 32 | // "any" interface and the DefaultService will be used. 33 | Addr string 34 | Writer api.Writer // Object used to send incoming ValueLists to. 35 | BufferSize uint16 // Maximum packet size to accept. 36 | PasswordLookup PasswordLookup // User to password lookup. 37 | SecurityLevel SecurityLevel // Minimal required security level. 38 | TypesDB *api.TypesDB // TypesDB for looking up DS names and verify data source types. 39 | // Interface is the name of the interface to use when subscribing to a 40 | // multicast group. Has no effect when using unicast. 41 | Interface string 42 | } 43 | 44 | // ListenAndWrite listens on the provided UDP connection (or creates one using 45 | // Addr if Conn is nil), parses the received packets and writes them to the 46 | // provided api.Writer. 47 | func (srv *Server) ListenAndWrite(ctx context.Context) error { 48 | ctx, cancel := context.WithCancel(ctx) 49 | defer cancel() 50 | 51 | if srv.Conn == nil { 52 | addr := srv.Addr 53 | if addr == "" { 54 | addr = ":" + DefaultService 55 | } 56 | 57 | laddr, err := net.ResolveUDPAddr("udp", addr) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if laddr.IP != nil && laddr.IP.IsMulticast() { 63 | var ifi *net.Interface 64 | if srv.Interface != "" { 65 | if ifi, err = net.InterfaceByName(srv.Interface); err != nil { 66 | return err 67 | } 68 | } 69 | srv.Conn, err = net.ListenMulticastUDP("udp", ifi, laddr) 70 | } else { 71 | srv.Conn, err = net.ListenUDP("udp", laddr) 72 | } 73 | if err != nil { 74 | return err 75 | } 76 | } 77 | 78 | if srv.BufferSize <= 0 { 79 | srv.BufferSize = DefaultBufferSize 80 | } 81 | 82 | popts := ParseOpts{ 83 | PasswordLookup: srv.PasswordLookup, 84 | SecurityLevel: srv.SecurityLevel, 85 | TypesDB: srv.TypesDB, 86 | } 87 | 88 | go func() { 89 | select { 90 | case <-ctx.Done(): 91 | // this interrupts the below Conn.Read(). 92 | srv.Conn.Close() 93 | } 94 | }() 95 | 96 | var wg sync.WaitGroup 97 | for { 98 | buf := make([]byte, srv.BufferSize) 99 | n, err := srv.Conn.Read(buf) 100 | if err != nil { 101 | srv.Conn.Close() 102 | wg.Wait() 103 | if ctx.Err() != nil { 104 | return ctx.Err() 105 | } 106 | return err 107 | } 108 | 109 | valueLists, err := Parse(buf[:n], popts) 110 | if err != nil { 111 | log.Printf("error while parsing: %v", err) 112 | continue 113 | } 114 | 115 | wg.Add(1) 116 | go func() { 117 | defer wg.Done() 118 | dispatch(ctx, valueLists, srv.Writer) 119 | }() 120 | } 121 | } 122 | 123 | func dispatch(ctx context.Context, valueLists []*api.ValueList, d api.Writer) { 124 | for _, vl := range valueLists { 125 | if err := d.Write(ctx, vl); err != nil { 126 | log.Printf("error while dispatching: %v", err) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /plugin/config.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 4 | // #cgo LDFLAGS: -ldl 5 | // #include 6 | // #include 7 | // #include 8 | // #include "plugin.h" 9 | // 10 | // /* work-around because Go can't deal with fields named "type". */ 11 | // static int config_value_type(oconfig_value_t *v) { 12 | // if (v == NULL) { 13 | // errno = EINVAL; 14 | // return -1; 15 | // } 16 | // return v->type; 17 | // } 18 | // 19 | // /* work-around because CGo has trouble accessing unions. */ 20 | // static char *config_value_string(oconfig_value_t *v) { 21 | // if (v == NULL || v->type != OCONFIG_TYPE_STRING) { 22 | // errno = EINVAL; 23 | // return NULL; 24 | // } 25 | // return v->value.string; 26 | // } 27 | // static double config_value_number(oconfig_value_t *v) { 28 | // if (v == NULL || v->type != OCONFIG_TYPE_NUMBER) { 29 | // errno = EINVAL; 30 | // return NAN; 31 | // } 32 | // return v->value.number; 33 | // } 34 | // static bool config_value_boolean(oconfig_value_t *v) { 35 | // if (v == NULL || v->type != OCONFIG_TYPE_BOOLEAN) { 36 | // errno = EINVAL; 37 | // return 0; 38 | // } 39 | // return v->value.boolean; 40 | // } 41 | import "C" 42 | 43 | import ( 44 | "fmt" 45 | "unsafe" 46 | 47 | "collectd.org/config" 48 | ) 49 | 50 | func unmarshalConfigBlocks(blocks *C.oconfig_item_t, blocksNum C.int) ([]config.Block, error) { 51 | var ret []config.Block 52 | for i := C.int(0); i < blocksNum; i++ { 53 | offset := uintptr(i) * C.sizeof_oconfig_item_t 54 | cBlock := (*C.oconfig_item_t)(unsafe.Pointer(uintptr(unsafe.Pointer(blocks)) + offset)) 55 | 56 | goBlock, err := unmarshalConfigBlock(cBlock) 57 | if err != nil { 58 | return nil, err 59 | } 60 | ret = append(ret, goBlock) 61 | } 62 | return ret, nil 63 | } 64 | 65 | func unmarshalConfigBlock(block *C.oconfig_item_t) (config.Block, error) { 66 | cfg := config.Block{ 67 | Key: C.GoString(block.key), 68 | } 69 | 70 | var err error 71 | if cfg.Values, err = unmarshalConfigValues(block.values, block.values_num); err != nil { 72 | return config.Block{}, err 73 | } 74 | 75 | if cfg.Children, err = unmarshalConfigBlocks(block.children, block.children_num); err != nil { 76 | return config.Block{}, err 77 | } 78 | 79 | return cfg, nil 80 | } 81 | 82 | func unmarshalConfigValues(values *C.oconfig_value_t, valuesNum C.int) ([]config.Value, error) { 83 | var ret []config.Value 84 | for i := C.int(0); i < valuesNum; i++ { 85 | offset := uintptr(i) * C.sizeof_oconfig_value_t 86 | cValue := (*C.oconfig_value_t)(unsafe.Pointer(uintptr(unsafe.Pointer(values)) + offset)) 87 | 88 | goValue, err := unmarshalConfigValue(cValue) 89 | if err != nil { 90 | return nil, err 91 | } 92 | ret = append(ret, goValue) 93 | } 94 | return ret, nil 95 | } 96 | 97 | func unmarshalConfigValue(value *C.oconfig_value_t) (config.Value, error) { 98 | typ, err := C.config_value_type(value) 99 | if err := wrapCError(0, err, "config_value_type"); err != nil { 100 | return config.Value{}, err 101 | } 102 | 103 | switch typ { 104 | case C.OCONFIG_TYPE_STRING: 105 | s, err := C.config_value_string(value) 106 | if err := wrapCError(0, err, "config_value_string"); err != nil { 107 | return config.Value{}, err 108 | } 109 | return config.String(C.GoString(s)), nil 110 | case C.OCONFIG_TYPE_NUMBER: 111 | n, err := C.config_value_number(value) 112 | if err := wrapCError(0, err, "config_value_number"); err != nil { 113 | return config.Value{}, err 114 | } 115 | return config.Float64(float64(n)), nil 116 | case C.OCONFIG_TYPE_BOOLEAN: 117 | b, err := C.config_value_boolean(value) 118 | if err := wrapCError(0, err, "config_value_boolean"); err != nil { 119 | return config.Value{}, err 120 | } 121 | return config.Bool(bool(b)), nil 122 | default: 123 | return config.Value{}, fmt.Errorf("unknown config value type: %d", typ) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /api/json.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "collectd.org/cdtime" 8 | "collectd.org/meta" 9 | ) 10 | 11 | // jsonValueList represents the format used by collectd's JSON export. 12 | type jsonValueList struct { 13 | Values []json.Number `json:"values"` 14 | DSTypes []string `json:"dstypes"` 15 | DSNames []string `json:"dsnames,omitempty"` 16 | Time cdtime.Time `json:"time"` 17 | Interval cdtime.Time `json:"interval"` 18 | Host string `json:"host"` 19 | Plugin string `json:"plugin"` 20 | PluginInstance string `json:"plugin_instance,omitempty"` 21 | Type string `json:"type"` 22 | TypeInstance string `json:"type_instance,omitempty"` 23 | Meta meta.Data `json:"meta,omitempty"` 24 | } 25 | 26 | // MarshalJSON implements the "encoding/json".Marshaler interface for 27 | // ValueList. 28 | func (vl *ValueList) MarshalJSON() ([]byte, error) { 29 | jvl := jsonValueList{ 30 | Values: make([]json.Number, len(vl.Values)), 31 | DSTypes: make([]string, len(vl.Values)), 32 | DSNames: make([]string, len(vl.Values)), 33 | Time: cdtime.New(vl.Time), 34 | Interval: cdtime.NewDuration(vl.Interval), 35 | Host: vl.Host, 36 | Plugin: vl.Plugin, 37 | PluginInstance: vl.PluginInstance, 38 | Type: vl.Type, 39 | TypeInstance: vl.TypeInstance, 40 | Meta: vl.Meta, 41 | } 42 | 43 | for i, v := range vl.Values { 44 | switch v := v.(type) { 45 | case Gauge: 46 | jvl.Values[i] = json.Number(fmt.Sprintf("%.15g", v)) 47 | case Derive: 48 | jvl.Values[i] = json.Number(fmt.Sprintf("%d", v)) 49 | case Counter: 50 | jvl.Values[i] = json.Number(fmt.Sprintf("%d", v)) 51 | default: 52 | return nil, fmt.Errorf("unexpected data source type: %T", v) 53 | } 54 | jvl.DSTypes[i] = v.Type() 55 | jvl.DSNames[i] = vl.DSName(i) 56 | } 57 | 58 | return json.Marshal(jvl) 59 | } 60 | 61 | // UnmarshalJSON implements the "encoding/json".Unmarshaler interface for 62 | // ValueList. 63 | // 64 | // Please note that this function is currently not compatible with write_http's 65 | // "StoreRates" setting: if enabled, write_http converts derives and counters 66 | // to a rate (a floating point number), but still puts "derive" or "counter" in 67 | // the "dstypes" array. UnmarshalJSON will try to parse such values as 68 | // integers, which will fail in many cases. 69 | func (vl *ValueList) UnmarshalJSON(data []byte) error { 70 | var jvl jsonValueList 71 | 72 | if err := json.Unmarshal(data, &jvl); err != nil { 73 | return err 74 | } 75 | 76 | vl.Host = jvl.Host 77 | vl.Plugin = jvl.Plugin 78 | vl.PluginInstance = jvl.PluginInstance 79 | vl.Type = jvl.Type 80 | vl.TypeInstance = jvl.TypeInstance 81 | 82 | vl.Time = jvl.Time.Time() 83 | vl.Interval = jvl.Interval.Duration() 84 | vl.Values = make([]Value, len(jvl.Values)) 85 | 86 | if len(jvl.Values) != len(jvl.DSTypes) { 87 | return fmt.Errorf("invalid data: %d value(s), %d data source type(s)", 88 | len(jvl.Values), len(jvl.DSTypes)) 89 | } 90 | 91 | for i, n := range jvl.Values { 92 | switch jvl.DSTypes[i] { 93 | case "gauge": 94 | v, err := n.Float64() 95 | if err != nil { 96 | return err 97 | } 98 | vl.Values[i] = Gauge(v) 99 | case "derive": 100 | v, err := n.Int64() 101 | if err != nil { 102 | return err 103 | } 104 | vl.Values[i] = Derive(v) 105 | case "counter": 106 | v, err := n.Int64() 107 | if err != nil { 108 | return err 109 | } 110 | vl.Values[i] = Counter(v) 111 | default: 112 | return fmt.Errorf("unexpected data source type: %q", jvl.DSTypes[i]) 113 | } 114 | } 115 | 116 | if len(jvl.DSNames) >= len(vl.Values) { 117 | vl.DSNames = make([]string, len(vl.Values)) 118 | copy(vl.DSNames, jvl.DSNames) 119 | } 120 | 121 | vl.Meta = jvl.Meta 122 | 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /exec/exec_x_test.go: -------------------------------------------------------------------------------- 1 | package exec_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "collectd.org/api" 12 | "collectd.org/exec" 13 | "github.com/google/go-cmp/cmp" 14 | "github.com/google/go-cmp/cmp/cmpopts" 15 | ) 16 | 17 | type testWriter struct { 18 | vl *api.ValueList 19 | } 20 | 21 | func (w *testWriter) Write(_ context.Context, vl *api.ValueList) error { 22 | if w.vl != nil { 23 | return errors.New("received unexpected second value") 24 | } 25 | 26 | w.vl = vl 27 | return nil 28 | } 29 | 30 | func TestValueCallback_ExecutorStop(t *testing.T) { 31 | cases := []struct { 32 | title string 33 | stopFunc func(f context.CancelFunc, e *exec.Executor) 34 | }{ 35 | {"ExecutorStop", func(_ context.CancelFunc, e *exec.Executor) { e.Stop() }}, 36 | {"CancelContext", func(cancel context.CancelFunc, _ *exec.Executor) { cancel() }}, 37 | } 38 | 39 | for _, tc := range cases { 40 | t.Run(tc.title, func(t *testing.T) { 41 | ctx, cancel := context.WithCancel(context.Background()) 42 | defer cancel() 43 | 44 | if err := os.Setenv("COLLECTD_HOSTNAME", "example.com"); err != nil { 45 | t.Fatal(err) 46 | } 47 | defer func() { 48 | os.Unsetenv("COLLECTD_HOSTNAME") 49 | }() 50 | 51 | savedPutval := exec.Putval 52 | defer func() { 53 | exec.Putval = savedPutval 54 | }() 55 | 56 | w := &testWriter{} 57 | exec.Putval = w 58 | 59 | e := exec.NewExecutor() 60 | ch := make(chan struct{}) 61 | go func() { 62 | // wait for ch to be closed 63 | <-ch 64 | tc.stopFunc(cancel, e) 65 | }() 66 | 67 | var once sync.Once 68 | e.ValueCallback(func() api.Value { 69 | once.Do(func() { 70 | close(ch) 71 | }) 72 | return api.Derive(42) 73 | }, &api.ValueList{ 74 | Identifier: api.Identifier{ 75 | Plugin: "go-exec", 76 | Type: "derive", 77 | }, 78 | Interval: time.Millisecond, 79 | DSNames: []string{"value"}, 80 | }) 81 | 82 | // e.Run() blocks until the context is canceled or 83 | // e.Stop() is called (see tc.stopFunc above). 84 | e.Run(ctx) 85 | 86 | want := &api.ValueList{ 87 | Identifier: api.Identifier{ 88 | Host: "example.com", 89 | Plugin: "go-exec", 90 | Type: "derive", 91 | }, 92 | Interval: time.Millisecond, 93 | Values: []api.Value{api.Derive(42)}, 94 | DSNames: []string{"value"}, 95 | } 96 | if diff := cmp.Diff(want, w.vl, cmpopts.IgnoreFields(api.ValueList{}, "Time")); diff != "" { 97 | t.Errorf("received value lists differ (+got/-want):\n%s", diff) 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func TestVoidCallback(t *testing.T) { 104 | cases := []struct { 105 | title string 106 | stopFunc func(f context.CancelFunc, e *exec.Executor) 107 | }{ 108 | {"ExecutorStop", func(_ context.CancelFunc, e *exec.Executor) { e.Stop() }}, 109 | {"CancelContext", func(cancel context.CancelFunc, _ *exec.Executor) { cancel() }}, 110 | } 111 | 112 | for _, tc := range cases { 113 | t.Run(tc.title, func(t *testing.T) { 114 | ctx, cancel := context.WithCancel(context.Background()) 115 | defer cancel() 116 | 117 | e := exec.NewExecutor() 118 | ch := make(chan struct{}) 119 | go func() { 120 | // wait for ch to be closed 121 | <-ch 122 | tc.stopFunc(cancel, e) 123 | }() 124 | 125 | var ( 126 | calls int 127 | once sync.Once 128 | ) 129 | e.VoidCallback(func(_ context.Context, d time.Duration) { 130 | if got, want := d, time.Millisecond; got != want { 131 | t.Errorf("VoidCallback(%v), want argument %v", got, want) 132 | } 133 | 134 | calls++ 135 | 136 | once.Do(func() { 137 | close(ch) 138 | }) 139 | }, time.Millisecond) 140 | 141 | // e.Run() blocks until the context is canceled or 142 | // e.Stop() is called (see tc.stopFunc above). 143 | e.Run(ctx) 144 | 145 | if got, want := calls, 1; got != want { 146 | t.Errorf("number of calls = %d, want %d", got, want) 147 | } 148 | }) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /export/export_test.go: -------------------------------------------------------------------------------- 1 | package export // import "collectd.org/export" 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "expvar" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "collectd.org/api" 12 | "github.com/google/go-cmp/cmp" 13 | "github.com/google/go-cmp/cmp/cmpopts" 14 | ) 15 | 16 | func TestDerive(t *testing.T) { 17 | // clean up shared resource after testing 18 | defer func() { 19 | vars = nil 20 | }() 21 | 22 | d := NewDeriveString("example.com/TestDerive/derive") 23 | for i := 0; i < 10; i++ { 24 | d.Add(i) 25 | } 26 | 27 | want := &api.ValueList{ 28 | Identifier: api.Identifier{ 29 | Host: "example.com", 30 | Plugin: "TestDerive", 31 | Type: "derive", 32 | }, 33 | Values: []api.Value{api.Derive(45)}, 34 | } 35 | got := d.ValueList() 36 | 37 | if diff := cmp.Diff(want, got); diff != "" { 38 | t.Errorf("Derive.ValueList() differs (+got/-want):\n%s", diff) 39 | } 40 | 41 | s := expvar.Get("example.com/TestDerive/derive").String() 42 | if s != "45" { 43 | t.Errorf("got %q, want %q", s, "45") 44 | } 45 | } 46 | 47 | func TestGauge(t *testing.T) { 48 | // clean up shared resource after testing 49 | defer func() { 50 | vars = nil 51 | }() 52 | 53 | g := NewGaugeString("example.com/TestGauge/gauge") 54 | g.Set(42.0) 55 | 56 | want := &api.ValueList{ 57 | Identifier: api.Identifier{ 58 | Host: "example.com", 59 | Plugin: "TestGauge", 60 | Type: "gauge", 61 | }, 62 | Values: []api.Value{api.Gauge(42)}, 63 | } 64 | got := g.ValueList() 65 | 66 | if diff := cmp.Diff(want, got); diff != "" { 67 | t.Errorf("Gauge.ValueList() differs (+got/-want):\n%s", diff) 68 | } 69 | 70 | s := expvar.Get("example.com/TestGauge/gauge").String() 71 | if s != "42" { 72 | t.Errorf("got %q, want %q", s, "42") 73 | } 74 | } 75 | 76 | type testWriter struct { 77 | got []*api.ValueList 78 | done chan<- struct{} 79 | once *sync.Once 80 | } 81 | 82 | func (w *testWriter) Write(ctx context.Context, vl *api.ValueList) error { 83 | w.got = append(w.got, vl) 84 | w.once.Do(func() { 85 | close(w.done) 86 | }) 87 | return nil 88 | } 89 | 90 | func TestRun(t *testing.T) { 91 | ctx, cancel := context.WithCancel(context.Background()) 92 | defer cancel() 93 | // clean up shared resource after testing 94 | defer func() { 95 | vars = nil 96 | }() 97 | 98 | d := NewDeriveString("example.com/TestRun/derive") 99 | d.Add(23) 100 | 101 | g := NewGaugeString("example.com/TestRun/gauge") 102 | g.Set(42) 103 | 104 | var ( 105 | done = make(chan struct{}) 106 | once sync.Once 107 | ) 108 | 109 | w := testWriter{ 110 | done: done, 111 | once: &once, 112 | } 113 | 114 | go func() { 115 | // when one metric has been written, cancel the context 116 | <-done 117 | cancel() 118 | }() 119 | 120 | err := Run(ctx, &w, Options{Interval: 100 * time.Millisecond}) 121 | if !errors.Is(err, context.Canceled) { 122 | t.Errorf("Run() = %v, want %v", err, context.Canceled) 123 | } 124 | 125 | want := []*api.ValueList{ 126 | { 127 | Identifier: api.Identifier{ 128 | Host: "example.com", 129 | Plugin: "TestRun", 130 | Type: "gauge", 131 | }, 132 | Time: time.Now(), 133 | Interval: 100 * time.Millisecond, 134 | Values: []api.Value{api.Gauge(42)}, 135 | }, 136 | { 137 | Identifier: api.Identifier{ 138 | Host: "example.com", 139 | Plugin: "TestRun", 140 | Type: "derive", 141 | }, 142 | Time: time.Now(), 143 | Interval: 100 * time.Millisecond, 144 | Values: []api.Value{api.Derive(23)}, 145 | }, 146 | } 147 | 148 | ignoreOrder := cmpopts.SortSlices(func(a, b *api.ValueList) bool { 149 | return a.Identifier.String() < b.Identifier.String() 150 | }) 151 | approximateTime := cmp.Comparer(func(t0, t1 time.Time) bool { 152 | diff := t0.Sub(t1) 153 | if t1.After(t0) { 154 | diff = t1.Sub(t0) 155 | } 156 | 157 | return diff < 2*time.Second 158 | }) 159 | if diff := cmp.Diff(want, w.got, ignoreOrder, approximateTime); diff != "" { 160 | t.Errorf("received value lists differ (+got/-want):\n%s", diff) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /plugin/log.go: -------------------------------------------------------------------------------- 1 | // +build go1.5,cgo 2 | 3 | package plugin // import "collectd.org/plugin" 4 | 5 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 6 | // #cgo LDFLAGS: -ldl 7 | // #include 8 | // #include 9 | // #include "plugin.h" 10 | // 11 | // static void (*plugin_log_) (int, char const *, ...) = NULL; 12 | // void wrap_plugin_log(int severity, char *msg) { 13 | // if (plugin_log_ == NULL) { 14 | // void *hnd = dlopen(NULL, RTLD_LAZY); 15 | // plugin_log_ = dlsym(hnd, "plugin_log"); 16 | // dlclose(hnd); 17 | // } 18 | // (*plugin_log_) (severity, "%s", msg); 19 | // } 20 | import "C" 21 | 22 | import ( 23 | "fmt" 24 | "strings" 25 | "unicode" 26 | "unsafe" 27 | ) 28 | 29 | // Severity is the severity of log messages. These are well-known constants 30 | // within collectd, so don't define your own. Use the constants provided by 31 | // this package instead. 32 | type Severity int 33 | 34 | // Predefined severities for collectd log functions. 35 | const ( 36 | SeverityError Severity = 3 37 | SeverityWarning Severity = 4 38 | SeverityNotice Severity = 5 39 | SeverityInfo Severity = 6 40 | SeverityDebug Severity = 7 41 | ) 42 | 43 | func log(s Severity, msg string) error { 44 | // Trim trailing whitespace. 45 | msg = strings.TrimRightFunc(msg, unicode.IsSpace) 46 | 47 | ptr := C.CString(msg) 48 | defer C.free(unsafe.Pointer(ptr)) 49 | 50 | _, err := C.wrap_plugin_log(C.int(s), ptr) 51 | return wrapCError(0, err, "plugin_log") 52 | } 53 | 54 | // Error logs an error using plugin_log(). Arguments are handled in the manner 55 | // of fmt.Print. 56 | func Error(v ...interface{}) error { 57 | return log(SeverityError, fmt.Sprint(v...)) 58 | } 59 | 60 | // Errorf logs an error using plugin_log(). Arguments are handled in the manner 61 | // of fmt.Printf. 62 | func Errorf(format string, v ...interface{}) error { 63 | return Error(fmt.Sprintf(format, v...)) 64 | } 65 | 66 | // Warning logs a warning using plugin_log(). Arguments are handled in the 67 | // manner of fmt.Print. 68 | func Warning(v ...interface{}) error { 69 | return log(SeverityWarning, fmt.Sprint(v...)) 70 | } 71 | 72 | // Warningf logs a warning using plugin_log(). Arguments are handled in the 73 | // manner of fmt.Printf. 74 | func Warningf(format string, v ...interface{}) error { 75 | return Warning(fmt.Sprintf(format, v...)) 76 | } 77 | 78 | // Notice logs a notice using plugin_log(). Arguments are handled in the manner 79 | // of fmt.Print. 80 | func Notice(v ...interface{}) error { 81 | return log(SeverityNotice, fmt.Sprint(v...)) 82 | } 83 | 84 | // Noticef logs a notice using plugin_log(). Arguments are handled in the 85 | // manner of fmt.Printf. 86 | func Noticef(format string, v ...interface{}) error { 87 | return Notice(fmt.Sprintf(format, v...)) 88 | } 89 | 90 | // Info logs a purely informal message using plugin_log(). Arguments are 91 | // handled in the manner of fmt.Print. 92 | func Info(v ...interface{}) error { 93 | return log(SeverityInfo, fmt.Sprint(v...)) 94 | } 95 | 96 | // Infof logs a purely informal message using plugin_log(). Arguments are 97 | // handled in the manner of fmt.Printf. 98 | func Infof(format string, v ...interface{}) error { 99 | return Info(fmt.Sprintf(format, v...)) 100 | } 101 | 102 | // Debug logs a debugging message using plugin_log(). Arguments are handled in 103 | // the manner of fmt.Print. 104 | func Debug(v ...interface{}) error { 105 | return log(SeverityDebug, fmt.Sprint(v...)) 106 | } 107 | 108 | // Debugf logs a debugging message using plugin_log(). Arguments are handled in 109 | // the manner of fmt.Printf. 110 | func Debugf(format string, v ...interface{}) error { 111 | return Debug(fmt.Sprintf(format, v...)) 112 | } 113 | 114 | // LogWriter implements the io.Writer interface on top of collectd's logging facility. 115 | type LogWriter Severity 116 | 117 | // Write converts p to a string and logs it with w's severity. 118 | func (w LogWriter) Write(p []byte) (n int, err error) { 119 | if err := log(Severity(w), string(p)); err != nil { 120 | return 0, err 121 | } 122 | return len(p), nil 123 | } 124 | -------------------------------------------------------------------------------- /exec/exec.go: -------------------------------------------------------------------------------- 1 | // Package exec implements tools to write plugins for collectd's "exec plugin" 2 | // in Go. 3 | package exec // import "collectd.org/exec" 4 | 5 | import ( 6 | "context" 7 | "log" 8 | "os" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "collectd.org/api" 14 | "collectd.org/format" 15 | ) 16 | 17 | // Putval is the dispatcher used by the exec package to print ValueLists. 18 | var Putval api.Writer = format.NewPutval(os.Stdout) 19 | 20 | type callback interface { 21 | run(context.Context, *sync.WaitGroup) 22 | stop() 23 | } 24 | 25 | // Executor holds one or more callbacks which are called periodically. 26 | type Executor struct { 27 | cb []callback 28 | group sync.WaitGroup 29 | } 30 | 31 | // NewExecutor returns a pointer to a new Executor object. 32 | func NewExecutor() *Executor { 33 | return &Executor{ 34 | group: sync.WaitGroup{}, 35 | } 36 | } 37 | 38 | // ValueCallback adds a simple "value" callback to the Executor. The callback 39 | // only returns a Number, i.e. either a api.Gauge or api.Derive, and formatting 40 | // and printing is done by the executor. 41 | func (e *Executor) ValueCallback(callback func() api.Value, vl *api.ValueList) { 42 | e.cb = append(e.cb, &valueCallback{ 43 | callback: callback, 44 | vl: *vl, 45 | done: make(chan struct{}), 46 | }) 47 | } 48 | 49 | // VoidCallback adds a "complex" callback to the Executor. While the functions 50 | // prototype is simpler, all the work has to be done by the callback, i.e. the 51 | // callback needs to format and print the appropriate lines to "STDOUT". 52 | // However, this allows cases in which the number of values reported varies, 53 | // e.g. depending on the system the code is running on. 54 | func (e *Executor) VoidCallback(callback func(context.Context, time.Duration), interval time.Duration) { 55 | e.cb = append(e.cb, voidCallback{ 56 | callback: callback, 57 | interval: interval, 58 | done: make(chan struct{}), 59 | }) 60 | } 61 | 62 | // Run starts calling all callbacks periodically and blocks. 63 | func (e *Executor) Run(ctx context.Context) { 64 | for _, cb := range e.cb { 65 | e.group.Add(1) 66 | go cb.run(ctx, &e.group) 67 | } 68 | 69 | e.group.Wait() 70 | } 71 | 72 | // Stop sends a signal to all callbacks to exit and returns. This unblocks 73 | // "Run()" but does not block itself. 74 | func (e *Executor) Stop() { 75 | for _, cb := range e.cb { 76 | cb.stop() 77 | } 78 | } 79 | 80 | type valueCallback struct { 81 | callback func() api.Value 82 | vl api.ValueList 83 | done chan struct{} 84 | } 85 | 86 | func (cb *valueCallback) run(ctx context.Context, g *sync.WaitGroup) { 87 | defer g.Done() 88 | 89 | if cb.vl.Host == "" { 90 | cb.vl.Host = Hostname() 91 | } 92 | cb.vl.Interval = sanitizeInterval(cb.vl.Interval) 93 | 94 | ticker := time.NewTicker(cb.vl.Interval) 95 | for { 96 | select { 97 | case <-ticker.C: 98 | cb.vl.Values = []api.Value{cb.callback()} 99 | cb.vl.Time = time.Now() 100 | Putval.Write(ctx, &cb.vl) 101 | case <-cb.done: 102 | return 103 | case <-ctx.Done(): 104 | return 105 | } 106 | } 107 | } 108 | 109 | func (cb *valueCallback) stop() { 110 | close(cb.done) 111 | } 112 | 113 | type voidCallback struct { 114 | callback func(context.Context, time.Duration) 115 | interval time.Duration 116 | done chan struct{} 117 | } 118 | 119 | func (cb voidCallback) run(ctx context.Context, g *sync.WaitGroup) { 120 | defer g.Done() 121 | 122 | ticker := time.NewTicker(sanitizeInterval(cb.interval)) 123 | 124 | for { 125 | select { 126 | case <-ticker.C: 127 | cb.callback(ctx, cb.interval) 128 | case <-cb.done: 129 | return 130 | case <-ctx.Done(): 131 | return 132 | } 133 | } 134 | } 135 | 136 | func (cb voidCallback) stop() { 137 | close(cb.done) 138 | } 139 | 140 | // Interval determines the default interval from the "COLLECTD_INTERVAL" 141 | // environment variable. It falls back to 10s if the environment variable is 142 | // unset or cannot be parsed. 143 | func Interval() time.Duration { 144 | i, err := strconv.ParseFloat(os.Getenv("COLLECTD_INTERVAL"), 64) 145 | if err != nil { 146 | log.Printf("unable to determine default interval: %v", err) 147 | return time.Second * 10 148 | } 149 | 150 | return time.Duration(i * float64(time.Second)) 151 | } 152 | 153 | // Hostname determines the hostname to use from the "COLLECTD_HOSTNAME" 154 | // environment variable and falls back to os.Hostname() if it is unset. If that 155 | // also fails an empty string is returned. 156 | func Hostname() string { 157 | if h := os.Getenv("COLLECTD_HOSTNAME"); h != "" { 158 | return h 159 | } 160 | 161 | if h, err := os.Hostname(); err == nil { 162 | return h 163 | } 164 | 165 | return "" 166 | } 167 | 168 | func sanitizeInterval(in time.Duration) time.Duration { 169 | if in == time.Duration(0) { 170 | return Interval() 171 | } 172 | 173 | return in 174 | } 175 | -------------------------------------------------------------------------------- /meta/meta.go: -------------------------------------------------------------------------------- 1 | // Package meta provides data types for collectd meta data. 2 | // 3 | // Meta data can be associated with value lists (api.ValueList) and 4 | // notifications (not yet implemented in the collectd Go API). 5 | package meta 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "math" 11 | ) 12 | 13 | type entryType int 14 | 15 | const ( 16 | _ entryType = iota 17 | metaStringType 18 | metaInt64Type 19 | metaUInt64Type 20 | metaFloat64Type 21 | metaBoolType 22 | ) 23 | 24 | // Data is a map of meta data values. No setter and getter methods are 25 | // implemented for this, callers are expected to add and remove entries as they 26 | // would from a normal map. 27 | type Data map[string]Entry 28 | 29 | // Clone returns a copy of d. 30 | func (d Data) Clone() Data { 31 | if d == nil { 32 | return nil 33 | } 34 | 35 | cpy := make(Data) 36 | for k, v := range d { 37 | cpy[k] = v 38 | } 39 | return cpy 40 | } 41 | 42 | // Entry is an entry in the metadata set. The typed value may be bool, float64, 43 | // int64, uint64, or string. 44 | type Entry struct { 45 | s string 46 | i int64 47 | u uint64 48 | f float64 49 | b bool 50 | 51 | typ entryType 52 | } 53 | 54 | // Bool returns a new bool Entry. 55 | func Bool(b bool) Entry { return Entry{b: b, typ: metaBoolType} } 56 | 57 | // Float64 returns a new float64 Entry. 58 | func Float64(f float64) Entry { return Entry{f: f, typ: metaFloat64Type} } 59 | 60 | // Int64 returns a new int64 Entry. 61 | func Int64(i int64) Entry { return Entry{i: i, typ: metaInt64Type} } 62 | 63 | // UInt64 returns a new uint64 Entry. 64 | func UInt64(u uint64) Entry { return Entry{u: u, typ: metaUInt64Type} } 65 | 66 | // String returns a new string Entry. 67 | func String(s string) Entry { return Entry{s: s, typ: metaStringType} } 68 | 69 | // Bool returns the bool value of e. 70 | func (e Entry) Bool() (value, ok bool) { return e.b, e.typ == metaBoolType } 71 | 72 | // Float64 returns the float64 value of e. 73 | func (e Entry) Float64() (float64, bool) { return e.f, e.typ == metaFloat64Type } 74 | 75 | // Int64 returns the int64 value of e. 76 | func (e Entry) Int64() (int64, bool) { return e.i, e.typ == metaInt64Type } 77 | 78 | // UInt64 returns the uint64 value of e. 79 | func (e Entry) UInt64() (uint64, bool) { return e.u, e.typ == metaUInt64Type } 80 | 81 | // String returns a string representation of e. 82 | func (e Entry) String() string { 83 | switch e.typ { 84 | case metaBoolType: 85 | return fmt.Sprintf("%v", e.b) 86 | case metaFloat64Type: 87 | return fmt.Sprintf("%.15g", e.f) 88 | case metaInt64Type: 89 | return fmt.Sprintf("%v", e.i) 90 | case metaUInt64Type: 91 | return fmt.Sprintf("%v", e.u) 92 | case metaStringType: 93 | return e.s 94 | default: 95 | return fmt.Sprintf("%v", nil) 96 | } 97 | } 98 | 99 | // IsString returns true if e is a string value. 100 | func (e Entry) IsString() bool { 101 | return e.typ == metaStringType 102 | } 103 | 104 | // Interface returns e's value. It is intended to be used with type switches 105 | // and when printing an entry's type with the "%T" formatting. 106 | func (e Entry) Interface() interface{} { 107 | switch e.typ { 108 | case metaBoolType: 109 | return e.b 110 | case metaFloat64Type: 111 | return e.f 112 | case metaInt64Type: 113 | return e.i 114 | case metaUInt64Type: 115 | return e.u 116 | case metaStringType: 117 | return e.s 118 | default: 119 | return nil 120 | } 121 | } 122 | 123 | // MarshalJSON implements the "encoding/json".Marshaller interface. 124 | func (e Entry) MarshalJSON() ([]byte, error) { 125 | switch e.typ { 126 | case metaBoolType: 127 | return json.Marshal(e.b) 128 | case metaFloat64Type: 129 | if math.IsNaN(e.f) { 130 | return json.Marshal(nil) 131 | } 132 | return json.Marshal(e.f) 133 | case metaInt64Type: 134 | return json.Marshal(e.i) 135 | case metaUInt64Type: 136 | return json.Marshal(e.u) 137 | case metaStringType: 138 | return json.Marshal(e.s) 139 | default: 140 | return json.Marshal(nil) 141 | } 142 | } 143 | 144 | // UnmarshalJSON implements the "encoding/json".Unmarshaller interface. 145 | func (e *Entry) UnmarshalJSON(raw []byte) error { 146 | var b *bool 147 | if json.Unmarshal(raw, &b) == nil && b != nil { 148 | *e = Bool(*b) 149 | return nil 150 | } 151 | 152 | var s *string 153 | if json.Unmarshal(raw, &s) == nil && s != nil { 154 | *e = String(*s) 155 | return nil 156 | } 157 | 158 | var i *int64 159 | if json.Unmarshal(raw, &i) == nil && i != nil { 160 | *e = Int64(*i) 161 | return nil 162 | } 163 | 164 | var u *uint64 165 | if json.Unmarshal(raw, &u) == nil && u != nil { 166 | *e = UInt64(*u) 167 | return nil 168 | } 169 | 170 | var f *float64 171 | if json.Unmarshal(raw, &f) == nil { 172 | if f != nil { 173 | *e = Float64(*f) 174 | } else { 175 | *e = Float64(math.NaN()) 176 | } 177 | return nil 178 | } 179 | 180 | return fmt.Errorf("unable to parse %q as meta entry", raw) 181 | } 182 | -------------------------------------------------------------------------------- /api/types_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestNewTypesDB(t *testing.T) { 13 | input := ` 14 | percent value:GAUGE:0:100.1 15 | total_bytes value:DERIVE:0:U 16 | signal_noise value:GAUGE:U:0 17 | mysql_qcache hits:COUNTER:0:U, inserts:COUNTER:0:U, not_cached:COUNTER:0:U, lowmem_prunes:COUNTER:0:U, queries_in_cache:GAUGE:0:U 18 | ` 19 | 20 | db, err := NewTypesDB(strings.NewReader(input)) 21 | if err != nil { 22 | t.Errorf("NewTypesDB() = %v, want %v", err, nil) 23 | } 24 | 25 | want := &DataSet{ 26 | Name: "percent", 27 | Sources: []DataSource{ 28 | { 29 | Name: "value", 30 | Type: reflect.TypeOf(Gauge(0)), 31 | Min: 0.0, 32 | Max: 100.1, 33 | }, 34 | }, 35 | } 36 | got, ok := db.DataSet("percent") 37 | if !ok || !reflect.DeepEqual(got, want) { 38 | t.Errorf("db[%q] = %v, %v, want %v, %v", "percent", got, ok, want, true) 39 | } 40 | 41 | got, ok = db.DataSet("total_bytes") 42 | if !ok { 43 | t.Fatal(`db.DataSet("total_bytes") missing`) 44 | } 45 | if !math.IsNaN(got.Sources[0].Max) { 46 | t.Errorf("got.Sources[0].Max = %g, want %g", got.Sources[0].Max, math.NaN()) 47 | } 48 | 49 | got, ok = db.DataSet("signal_noise") 50 | if !ok { 51 | t.Fatal(`db.DataSet("signal_noise") missing`) 52 | } 53 | if !math.IsNaN(got.Sources[0].Min) { 54 | t.Errorf("got.Sources[0].Min = %g, want %g", got.Sources[0].Min, math.NaN()) 55 | } 56 | 57 | got, ok = db.DataSet("mysql_qcache") 58 | if !ok { 59 | t.Fatal(`db.DataSet("mysql_qcache") missing`) 60 | } 61 | if len(got.Sources) != 5 { 62 | t.Errorf("len(got.Sources) = %d, want %d", len(got.Sources), 5) 63 | } 64 | for i, name := range []string{"hits", "inserts", "not_cached", "lowmem_prunes", "queries_in_cache"} { 65 | if got.Sources[i].Name != name { 66 | t.Errorf("got.Sources[%d].Name = %q, want %q", i, got.Sources[i].Name, name) 67 | } 68 | } 69 | } 70 | 71 | func TestTypesDB_ValueList(t *testing.T) { 72 | db, err := NewTypesDB(strings.NewReader(` 73 | counter value:COUNTER:U:U 74 | gauge value:GAUGE:U:U 75 | derive value:DERIVE:0:U 76 | if_octets rx:DERIVE:0:U, tx:DERIVE:0:U 77 | `)) 78 | if err != nil { 79 | t.Errorf("NewTypesDB() = %v, want %v", err, nil) 80 | } 81 | 82 | id := Identifier{ 83 | Host: "example.com", 84 | Plugin: "golang", 85 | Type: "gauge", 86 | } 87 | vl, err := db.ValueList(id, time.Unix(1469175855, 0), 10*time.Second, Gauge(42)) 88 | if err != nil { 89 | t.Errorf("db.Values(%v, %v, %v, %v) = (%v, %v), want (..., %v)", id, time.Unix(1469175855, 0), 10*time.Second, Gauge(42.0), vl, err, nil) 90 | } 91 | 92 | } 93 | 94 | func TestDataSource_Value(t *testing.T) { 95 | cases := []struct { 96 | arg interface{} 97 | typ reflect.Type 98 | wantValue Value 99 | wantErr bool 100 | }{ 101 | // COUNTER 102 | {int(42), dsTypeCounter, Counter(42), false}, 103 | {uint(42), dsTypeCounter, Counter(42), false}, 104 | {int64(42), dsTypeCounter, Counter(42), false}, 105 | {uint64(42), dsTypeCounter, Counter(42), false}, 106 | {float32(42.5), dsTypeCounter, Counter(42), false}, 107 | {float64(42.8), dsTypeCounter, Counter(42), false}, 108 | {Counter(42), dsTypeCounter, Counter(42), false}, 109 | {true, dsTypeCounter, nil, true}, 110 | {"42", dsTypeCounter, nil, true}, 111 | // DERIVE 112 | {int(42), dsTypeDerive, Derive(42), false}, 113 | {uint(42), dsTypeDerive, Derive(42), false}, 114 | {int64(42), dsTypeDerive, Derive(42), false}, 115 | {uint64(42), dsTypeDerive, Derive(42), false}, 116 | {float32(42.5), dsTypeDerive, Derive(42), false}, 117 | {float64(42.8), dsTypeDerive, Derive(42), false}, 118 | {Derive(42), dsTypeDerive, Derive(42), false}, 119 | {true, dsTypeDerive, nil, true}, 120 | {"42", dsTypeDerive, nil, true}, 121 | // GAUGE 122 | {int(42), dsTypeGauge, Gauge(42), false}, 123 | {uint(42), dsTypeGauge, Gauge(42), false}, 124 | {int64(42), dsTypeGauge, Gauge(42), false}, 125 | {uint64(42), dsTypeGauge, Gauge(42), false}, 126 | {float32(42.5), dsTypeGauge, Gauge(42.5), false}, 127 | {float64(42.8), dsTypeGauge, Gauge(42.8), false}, 128 | {Gauge(42.9), dsTypeGauge, Gauge(42.9), false}, 129 | {true, dsTypeGauge, nil, true}, 130 | {"42", dsTypeGauge, nil, true}, 131 | } 132 | 133 | for _, c := range cases { 134 | dsrc := DataSource{ 135 | Name: "value", 136 | Type: c.typ, 137 | Min: 0.0, 138 | Max: math.NaN(), 139 | } 140 | 141 | got, err := dsrc.Value(c.arg) 142 | if err != nil { 143 | if !c.wantErr { 144 | t.Errorf("dsrc.Type = %s; dsrc.Value(%v) = (%v, %v), want ", dsrc.Type.Name(), c.arg, got, err) 145 | } 146 | continue 147 | } 148 | 149 | if c.wantErr || !reflect.DeepEqual(got, c.wantValue) { 150 | var wantErr error 151 | if c.wantErr { 152 | wantErr = errors.New("") 153 | } 154 | 155 | t.Errorf("dsrc.Type = %s; dsrc.Value(%v) = (%v, %v), want (%v, %v)", dsrc.Type.Name(), c.arg, got, err, c.wantValue, wantErr) 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /network/buffer_test.go: -------------------------------------------------------------------------------- 1 | package network // import "collectd.org/network" 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "math" 8 | "reflect" 9 | "testing" 10 | "time" 11 | 12 | "collectd.org/api" 13 | ) 14 | 15 | func TestWriteValueList(t *testing.T) { 16 | ctx := context.Background() 17 | b := NewBuffer(0) 18 | 19 | vl := &api.ValueList{ 20 | Identifier: api.Identifier{ 21 | Host: "example.com", 22 | Plugin: "golang", 23 | Type: "gauge", 24 | }, 25 | Time: time.Unix(1426076671, 123000000), // Wed Mar 11 13:24:31 CET 2015 26 | Interval: 10 * time.Second, 27 | Values: []api.Value{api.Derive(1)}, 28 | } 29 | 30 | if err := b.Write(ctx, vl); err != nil { 31 | t.Errorf("Write got %v, want nil", err) 32 | return 33 | } 34 | 35 | // ValueList with much the same fields, to test compression. 36 | vl = &api.ValueList{ 37 | Identifier: api.Identifier{ 38 | Host: "example.com", 39 | Plugin: "golang", 40 | PluginInstance: "test", 41 | Type: "gauge", 42 | }, 43 | Time: time.Unix(1426076681, 234000000), // Wed Mar 11 13:24:41 CET 2015 44 | Interval: 10 * time.Second, 45 | Values: []api.Value{api.Derive(2)}, 46 | } 47 | 48 | if err := b.Write(ctx, vl); err != nil { 49 | t.Errorf("Write got %v, want nil", err) 50 | return 51 | } 52 | 53 | want := []byte{ 54 | // vl1 55 | 0, 0, 0, 16, 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', 0, 56 | 0, 2, 0, 11, 'g', 'o', 'l', 'a', 'n', 'g', 0, 57 | 0, 4, 0, 10, 'g', 'a', 'u', 'g', 'e', 0, 58 | // 1426076671.123 * 2^30 = 1531238166015458148.352 59 | // 1531238166015458148 = 0x15400cffc7df3b64 60 | 0, 8, 0, 12, 0x15, 0x40, 0x0c, 0xff, 0xc7, 0xdf, 0x3b, 0x64, 61 | 0, 9, 0, 12, 0, 0, 0, 0x02, 0x80, 0, 0, 0, 62 | 0, 6, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 1, 63 | // vl2 64 | 0, 3, 0, 9, 't', 'e', 's', 't', 0, 65 | // 1426076681.234 * 2^30 = 1531238176872061730.816 66 | // 1531238176872061731 = 0x15400d024ef9db23 67 | 0, 8, 0, 12, 0x15, 0x40, 0x0d, 0x02, 0x4e, 0xf9, 0xdb, 0x23, 68 | 0, 6, 0, 15, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 2, 69 | } 70 | got := b.buffer.Bytes() 71 | 72 | if !reflect.DeepEqual(got, want) { 73 | t.Errorf("got %v, want %v", got, want) 74 | } 75 | } 76 | 77 | func TestWriteTime(t *testing.T) { 78 | b := &Buffer{buffer: new(bytes.Buffer), size: DefaultBufferSize} 79 | b.writeTime(time.Unix(1426083986, 314000000)) // Wed Mar 11 15:26:26 CET 2015 80 | 81 | // 1426083986.314 * 2^30 = 1531246020641985396.736 82 | // 1531246020641985397 = 0x1540142494189375 83 | want := []byte{0, 8, // pkg type 84 | 0, 12, // pkg len 85 | 0x15, 0x40, 0x14, 0x24, 0x94, 0x18, 0x93, 0x75, 86 | } 87 | got := b.buffer.Bytes() 88 | 89 | if !reflect.DeepEqual(got, want) { 90 | t.Errorf("got %v, want %v", got, want) 91 | } 92 | } 93 | 94 | func TestWriteValues(t *testing.T) { 95 | b := &Buffer{buffer: new(bytes.Buffer), size: DefaultBufferSize} 96 | 97 | b.writeValues([]api.Value{ 98 | api.Gauge(42), 99 | api.Derive(31337), 100 | api.Gauge(math.NaN()), 101 | }) 102 | 103 | want := []byte{0, 6, // pkg type 104 | 0, 33, // pkg len 105 | 0, 3, // num values 106 | 1, 2, 1, // gauge, derive, gauge 107 | 0, 0, 0, 0, 0, 0, 0x45, 0x40, // 42.0 108 | 0, 0, 0, 0, 0, 0, 0x7a, 0x69, // 31337 109 | 0, 0, 0, 0, 0, 0, 0xf8, 0x7f, // NaN 110 | } 111 | got := b.buffer.Bytes() 112 | 113 | if !reflect.DeepEqual(got, want) { 114 | t.Errorf("got %v, want %v", got, want) 115 | } 116 | } 117 | 118 | func TestWriteString(t *testing.T) { 119 | b := &Buffer{buffer: new(bytes.Buffer), size: DefaultBufferSize} 120 | 121 | if err := b.writeString(0xf007, "foo"); err != nil { 122 | t.Errorf("got %v, want nil", err) 123 | } 124 | 125 | want := []byte{0xf0, 0x07, // pkg type 126 | 0, 8, // pkg len 127 | 'f', 'o', 'o', 0, // "foo\0" 128 | } 129 | got := b.buffer.Bytes() 130 | 131 | if !reflect.DeepEqual(got, want) { 132 | t.Errorf("got %v, want %v", got, want) 133 | } 134 | } 135 | 136 | func TestWriteInt(t *testing.T) { 137 | b := &Buffer{buffer: new(bytes.Buffer), size: DefaultBufferSize} 138 | 139 | if err := b.writeInt(23, uint64(384)); err != nil { 140 | t.Errorf("got %v, want nil", err) 141 | } 142 | 143 | want := []byte{0, 23, // pkg type 144 | 0, 12, // pkg len 145 | 0, 0, 0, 0, 0, 0, 1, 128, // 384 146 | } 147 | got := b.buffer.Bytes() 148 | 149 | if !reflect.DeepEqual(got, want) { 150 | t.Errorf("got %v, want %v", got, want) 151 | } 152 | } 153 | 154 | // unknownType implements the api.Value interface. 155 | type unknownType int 156 | 157 | func (v unknownType) Type() string { return "unknown" } 158 | 159 | func TestUnknownType(t *testing.T) { 160 | ctx := context.Background() 161 | vl := &api.ValueList{ 162 | Identifier: api.Identifier{ 163 | Host: "example.com", 164 | Plugin: "golang", 165 | PluginInstance: "test", 166 | Type: "unknown", 167 | }, 168 | Time: time.Unix(1426076681, 234000000), // Wed Mar 11 13:24:41 CET 2015 169 | Interval: 10 * time.Second, 170 | Values: []api.Value{unknownType(2)}, 171 | } 172 | 173 | s1 := NewBuffer(0) 174 | if err := s1.Write(ctx, vl); !errors.Is(err, ErrUnknownType) { 175 | t.Errorf("Buffer.Write(%v) = %v, want %v", vl, err, ErrUnknownType) 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /export/export.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package export provides an interface to instrument Go code. 3 | 4 | Instrumenting Go code with this package is very similar to the "expvar" package 5 | in the vanilla Go distribution. In fact, the variables exported with this 6 | package are also registered with the "expvar" package, so that you can also use 7 | other existing metric collection frameworks with it. This package differs in 8 | that it has an explicitly cumulative type, Derive. 9 | 10 | The intended usage pattern of this package is as follows: First, global 11 | variables are initialized with NewDerive(), NewGauge(), NewDeriveString() or 12 | NewGaugeString(). The Run() function is called as a separate goroutine as part 13 | of your program's initialization and, last but not least, the variables are 14 | updated with their respective update functions, Add() for Derive and Set() for 15 | Gauge. 16 | 17 | // Initialize global variable. 18 | var requestCounter = export.NewDeriveString("example.com/golang/total_requests") 19 | 20 | // Call Run() in its own goroutine. 21 | func main() { 22 | ctx := context.Background() 23 | 24 | // Any other type implementing api.Writer works. 25 | client, err := network.Dial( 26 | net.JoinHostPort(network.DefaultIPv6Address, network.DefaultService), 27 | network.ClientOptions{}) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | go export.Run(ctx, client, export.Options{ 32 | Interval: 10 * time.Second, 33 | }) 34 | // … 35 | } 36 | 37 | // Update variable. 38 | func requestHandler(w http.ResponseWriter, req *http.Request) { 39 | defer requestCounter.Add(1) 40 | // … 41 | } 42 | */ 43 | package export // import "collectd.org/export" 44 | 45 | import ( 46 | "context" 47 | "expvar" 48 | "log" 49 | "math" 50 | "strconv" 51 | "sync" 52 | "time" 53 | 54 | "collectd.org/api" 55 | ) 56 | 57 | var ( 58 | mutex sync.RWMutex 59 | vars []Var 60 | ) 61 | 62 | // Var is an abstract type for metrics exported by this package. 63 | type Var interface { 64 | ValueList() *api.ValueList 65 | } 66 | 67 | // Publish adds v to the internal list of exported metrics. 68 | func Publish(v Var) { 69 | mutex.Lock() 70 | defer mutex.Unlock() 71 | 72 | vars = append(vars, v) 73 | } 74 | 75 | // Options holds options for the Run() function. 76 | type Options struct { 77 | Interval time.Duration 78 | } 79 | 80 | // Run periodically calls the ValueList function of each Var, sets the Time and 81 | // Interval fields and passes it w.Write(). This function blocks until the 82 | // context is cancelled. 83 | func Run(ctx context.Context, w api.Writer, opts Options) error { 84 | ticker := time.NewTicker(opts.Interval) 85 | 86 | for { 87 | select { 88 | case <-ticker.C: 89 | mutex.RLock() 90 | for _, v := range vars { 91 | vl := v.ValueList() 92 | vl.Time = time.Now() 93 | vl.Interval = opts.Interval 94 | if err := w.Write(ctx, vl); err != nil { 95 | log.Printf("%T.Write(): %v", w, err) 96 | } 97 | } 98 | mutex.RUnlock() 99 | case <-ctx.Done(): 100 | return ctx.Err() 101 | } 102 | } 103 | } 104 | 105 | // Derive represents a cumulative integer data type, for example "requests 106 | // served since server start". It implements the Var and expvar.Var interfaces. 107 | type Derive struct { 108 | mu sync.RWMutex 109 | id api.Identifier 110 | value api.Derive 111 | } 112 | 113 | // NewDerive initializes a new Derive, registers it with the "expvar" package 114 | // and returns it. The initial value is zero. 115 | func NewDerive(id api.Identifier) *Derive { 116 | d := &Derive{ 117 | id: id, 118 | value: 0, 119 | } 120 | 121 | Publish(d) 122 | expvar.Publish(id.String(), d) 123 | return d 124 | } 125 | 126 | // NewDeriveString parses s as an Identifier and returns a new Derive. If 127 | // parsing s fails, it will panic. This simplifies initializing global 128 | // variables. 129 | func NewDeriveString(s string) *Derive { 130 | id, err := api.ParseIdentifier(s) 131 | if err != nil { 132 | log.Fatal(err) 133 | } 134 | 135 | return NewDerive(id) 136 | } 137 | 138 | // Add adds diff to d. 139 | func (d *Derive) Add(diff int) { 140 | d.mu.Lock() 141 | defer d.mu.Unlock() 142 | d.value += api.Derive(diff) 143 | } 144 | 145 | // String returns the string representation of d. 146 | func (d *Derive) String() string { 147 | d.mu.RLock() 148 | defer d.mu.RUnlock() 149 | return strconv.FormatInt(int64(d.value), 10) 150 | } 151 | 152 | // ValueList returns the ValueList representation of d. Both, Time and Interval 153 | // are set to zero. 154 | func (d *Derive) ValueList() *api.ValueList { 155 | d.mu.RLock() 156 | defer d.mu.RUnlock() 157 | return &api.ValueList{ 158 | Identifier: d.id, 159 | Values: []api.Value{d.value}, 160 | } 161 | } 162 | 163 | // Gauge represents an absolute floating point data type, for example "heap 164 | // memory used". It implements the Var and expvar.Var interfaces. 165 | type Gauge struct { 166 | mu sync.RWMutex 167 | id api.Identifier 168 | value api.Gauge 169 | } 170 | 171 | // NewGauge initializes a new Gauge, registers it with the "expvar" package and 172 | // returns it. The initial value is NaN. 173 | func NewGauge(id api.Identifier) *Gauge { 174 | g := &Gauge{ 175 | id: id, 176 | value: api.Gauge(math.NaN()), 177 | } 178 | 179 | Publish(g) 180 | expvar.Publish(id.String(), g) 181 | return g 182 | } 183 | 184 | // NewGaugeString parses s as an Identifier and returns a new Gauge. If parsing 185 | // s fails, it will panic. This simplifies initializing global variables. 186 | func NewGaugeString(s string) *Gauge { 187 | id, err := api.ParseIdentifier(s) 188 | if err != nil { 189 | log.Fatal(err) 190 | } 191 | 192 | return NewGauge(id) 193 | } 194 | 195 | // Set sets g to v. 196 | func (g *Gauge) Set(v float64) { 197 | g.mu.Lock() 198 | defer g.mu.Unlock() 199 | g.value = api.Gauge(v) 200 | } 201 | 202 | // String returns the string representation of g. 203 | func (g *Gauge) String() string { 204 | g.mu.RLock() 205 | defer g.mu.RUnlock() 206 | return strconv.FormatFloat(float64(g.value), 'g', -1, 64) 207 | } 208 | 209 | // ValueList returns the ValueList representation of g. Both, Time and Interval 210 | // are set to zero. 211 | func (g *Gauge) ValueList() *api.ValueList { 212 | g.mu.RLock() 213 | defer g.mu.RUnlock() 214 | return &api.ValueList{ 215 | Identifier: g.id, 216 | Values: []api.Value{g.value}, 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /network/crypto.go: -------------------------------------------------------------------------------- 1 | package network // import "collectd.org/network" 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/aes" 7 | "crypto/cipher" 8 | "crypto/hmac" 9 | "crypto/rand" 10 | "crypto/sha1" 11 | "crypto/sha256" 12 | "encoding/binary" 13 | "errors" 14 | "fmt" 15 | "io" 16 | "log" 17 | "os" 18 | "strings" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | // PasswordLookup is used when parsing signed and encrypted network traffic to 24 | // look up the password associated with a given username. 25 | type PasswordLookup interface { 26 | Password(user string) (string, error) 27 | } 28 | 29 | // AuthFile implements the PasswordLookup interface in the same way the 30 | // collectd network plugin implements it, i.e. by stat'ing and reading a file. 31 | // 32 | // The file has a very simple syntax with one username / password mapping per 33 | // line, separated by a colon. For example: 34 | // 35 | // alice: w0nderl4nd 36 | // bob: bu1|der 37 | type AuthFile struct { 38 | name string 39 | last time.Time 40 | data map[string]string 41 | lock *sync.Mutex 42 | } 43 | 44 | // NewAuthFile initializes and returns a new AuthFile. 45 | func NewAuthFile(name string) *AuthFile { 46 | return &AuthFile{ 47 | name: name, 48 | lock: &sync.Mutex{}, 49 | } 50 | } 51 | 52 | // Password looks up a user in the file and returns the associated password. 53 | func (a *AuthFile) Password(user string) (string, error) { 54 | if a == nil { 55 | return "", fmt.Errorf("no AuthFile") 56 | } 57 | 58 | a.lock.Lock() 59 | defer a.lock.Unlock() 60 | 61 | if err := a.update(); err != nil { 62 | return "", err 63 | } 64 | 65 | pwd, ok := a.data[user] 66 | if !ok { 67 | return "", fmt.Errorf("no such user: %q", user) 68 | } 69 | 70 | return pwd, nil 71 | } 72 | 73 | func (a *AuthFile) update() error { 74 | fi, err := os.Stat(a.name) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | if !fi.ModTime().After(a.last) { 80 | // up to date 81 | return nil 82 | } 83 | 84 | file, err := os.Open(a.name) 85 | if err != nil { 86 | return err 87 | } 88 | defer file.Close() 89 | 90 | newData := make(map[string]string) 91 | 92 | r := bufio.NewReader(file) 93 | for { 94 | line, err := r.ReadString('\n') 95 | if err != nil && err != io.EOF { 96 | return err 97 | } else if err == io.EOF { 98 | break 99 | } 100 | 101 | line = strings.Trim(line, " \r\n\t\v") 102 | fields := strings.SplitN(line, ":", 2) 103 | if len(fields) != 2 { 104 | continue 105 | } 106 | 107 | user := strings.TrimSpace(fields[0]) 108 | pass := strings.TrimSpace(fields[1]) 109 | if strings.HasPrefix(user, "#") { 110 | continue 111 | } 112 | 113 | newData[user] = pass 114 | } 115 | 116 | a.data = newData 117 | a.last = fi.ModTime() 118 | return nil 119 | } 120 | 121 | func signSHA256(payload []byte, username, password string) []byte { 122 | mac := hmac.New(sha256.New, bytes.NewBufferString(password).Bytes()) 123 | 124 | usernameBuffer := bytes.NewBufferString(username) 125 | 126 | size := uint16(36 + usernameBuffer.Len()) 127 | 128 | mac.Write(usernameBuffer.Bytes()) 129 | mac.Write(payload) 130 | 131 | out := new(bytes.Buffer) 132 | binary.Write(out, binary.BigEndian, uint16(typeSignSHA256)) 133 | binary.Write(out, binary.BigEndian, size) 134 | out.Write(mac.Sum(nil)) 135 | out.Write(usernameBuffer.Bytes()) 136 | out.Write(payload) 137 | 138 | return out.Bytes() 139 | } 140 | 141 | func verifySHA256(part, payload []byte, lookup PasswordLookup) (bool, error) { 142 | if lookup == nil { 143 | return false, errors.New("no PasswordLookup available") 144 | } 145 | 146 | if len(part) <= 32 { 147 | return false, fmt.Errorf("part too small (%d bytes)", len(part)) 148 | } 149 | 150 | hash := part[:32] 151 | user := bytes.NewBuffer(part[32:]).String() 152 | 153 | password, err := lookup.Password(user) 154 | if err != nil { 155 | return false, err 156 | } 157 | 158 | mac := hmac.New(sha256.New, bytes.NewBufferString(password).Bytes()) 159 | 160 | mac.Write(part[32:]) 161 | mac.Write(payload) 162 | 163 | return bytes.Equal(hash, mac.Sum(nil)), nil 164 | } 165 | 166 | func createCipher(password string, iv []byte) (cipher.Stream, error) { 167 | passwordHash := sha256.Sum256(bytes.NewBufferString(password).Bytes()) 168 | 169 | blockCipher, err := aes.NewCipher(passwordHash[:]) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | streamCipher := cipher.NewOFB(blockCipher, iv) 175 | return streamCipher, nil 176 | } 177 | 178 | func encryptAES256(plaintext []byte, username, password string) ([]byte, error) { 179 | iv := make([]byte, 16) 180 | if _, err := rand.Read(iv); err != nil { 181 | log.Printf("rand.Read: %v", err) 182 | return nil, err 183 | } 184 | 185 | streamCipher, err := createCipher(password, iv) 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | usernameBuffer := bytes.NewBufferString(username) 191 | 192 | size := uint16(42 + usernameBuffer.Len() + len(plaintext)) 193 | 194 | checksum := sha1.Sum(plaintext) 195 | 196 | out := new(bytes.Buffer) 197 | binary.Write(out, binary.BigEndian, uint16(typeEncryptAES256)) 198 | binary.Write(out, binary.BigEndian, size) 199 | binary.Write(out, binary.BigEndian, uint16(usernameBuffer.Len())) 200 | out.Write(usernameBuffer.Bytes()) 201 | out.Write(iv) 202 | 203 | w := &cipher.StreamWriter{S: streamCipher, W: out} 204 | w.Write(checksum[:]) 205 | w.Write(plaintext) 206 | 207 | return out.Bytes(), nil 208 | } 209 | 210 | func decryptAES256(ciphertext []byte, lookup PasswordLookup) ([]byte, error) { 211 | if lookup == nil { 212 | return nil, errors.New("no PasswordLookup available") 213 | } 214 | if len(ciphertext) < 2 { 215 | return nil, errors.New("buffer too short") 216 | } 217 | 218 | buf := bytes.NewBuffer(ciphertext) 219 | userLen := int(binary.BigEndian.Uint16(buf.Next(2))) 220 | if 42+userLen >= buf.Len() { 221 | return nil, fmt.Errorf("invalid username length %d", userLen) 222 | } 223 | user := bytes.NewBuffer(buf.Next(userLen)).String() 224 | 225 | password, err := lookup.Password(user) 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | iv := make([]byte, 16) 231 | if n, err := buf.Read(iv); n != 16 || err != nil { 232 | return nil, fmt.Errorf("reading IV failed: %v", err) 233 | } 234 | 235 | streamCipher, err := createCipher(password, iv) 236 | if err != nil { 237 | return nil, err 238 | } 239 | 240 | r := &cipher.StreamReader{S: streamCipher, R: buf} 241 | 242 | plaintext := make([]byte, buf.Len()) 243 | if n, err := r.Read(plaintext); n != len(plaintext) || err != nil { 244 | return nil, fmt.Errorf("decryption failure: got (%d, %v), want (%d, nil)", n, err, len(plaintext)) 245 | } 246 | 247 | checksumWant := plaintext[:20] 248 | plaintext = plaintext[20:] 249 | checksumGot := sha1.Sum(plaintext) 250 | 251 | if !bytes.Equal(checksumGot[:], checksumWant[:]) { 252 | return nil, errors.New("checksum mismatch") 253 | } 254 | 255 | return plaintext, nil 256 | } 257 | -------------------------------------------------------------------------------- /plugin/generator/generator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "sort" 11 | "strings" 12 | "text/template" 13 | ) 14 | 15 | var functions = []Function{ 16 | { 17 | Name: "plugin_register_complex_read", 18 | Args: []Argument{ 19 | {"group", "meta_data_t *"}, 20 | {"name", "char const *"}, 21 | {"callback", "plugin_read_cb"}, 22 | {"interval", "cdtime_t"}, 23 | {"ud", "user_data_t *"}, 24 | }, 25 | Ret: "int", 26 | }, 27 | { 28 | Name: "plugin_register_write", 29 | Args: []Argument{ 30 | {"name", "char const *"}, 31 | {"callback", "plugin_write_cb"}, 32 | {"ud", "user_data_t *"}, 33 | }, 34 | Ret: "int", 35 | }, 36 | { 37 | Name: "plugin_register_shutdown", 38 | Args: []Argument{ 39 | {"name", "char const *"}, 40 | {"callback", "plugin_shutdown_cb"}, 41 | }, 42 | Ret: "int", 43 | }, 44 | { 45 | Name: "plugin_register_log", 46 | Args: []Argument{ 47 | {"name", "char const *"}, 48 | {"callback", "plugin_log_cb"}, 49 | {"ud", "user_data_t *"}, 50 | }, 51 | Ret: "int", 52 | }, 53 | { 54 | Name: "plugin_dispatch_values", 55 | Args: []Argument{ 56 | {"vl", "value_list_t const *"}, 57 | }, 58 | Ret: "int", 59 | }, 60 | { 61 | Name: "plugin_get_interval", 62 | Ret: "cdtime_t", 63 | }, 64 | { 65 | Name: "meta_data_create", 66 | Ret: "meta_data_t *", 67 | }, 68 | { 69 | Name: "meta_data_destroy", 70 | Args: []Argument{ 71 | {"md", "meta_data_t *"}, 72 | }, 73 | Ret: "void", 74 | }, 75 | { 76 | Name: "meta_data_toc", 77 | Args: []Argument{ 78 | {"md", "meta_data_t *"}, 79 | {"toc", "char ***"}, 80 | }, 81 | Ret: "int", 82 | }, 83 | { 84 | Name: "meta_data_type", 85 | Args: []Argument{ 86 | {"md", "meta_data_t *"}, 87 | {"key", "char const *"}, 88 | }, 89 | Ret: "int", 90 | }, 91 | { 92 | Name: "meta_data_add_boolean", 93 | Args: []Argument{ 94 | {"md", "meta_data_t *"}, 95 | {"key", "char const *"}, 96 | {"value", "bool"}, 97 | }, 98 | Ret: "int", 99 | }, 100 | { 101 | Name: "meta_data_add_double", 102 | Args: []Argument{ 103 | {"md", "meta_data_t *"}, 104 | {"key", "char const *"}, 105 | {"value", "double"}, 106 | }, 107 | Ret: "int", 108 | }, 109 | { 110 | Name: "meta_data_add_signed_int", 111 | Args: []Argument{ 112 | {"md", "meta_data_t *"}, 113 | {"key", "char const *"}, 114 | {"value", "int64_t"}, 115 | }, 116 | Ret: "int", 117 | }, 118 | { 119 | Name: "meta_data_add_unsigned_int", 120 | Args: []Argument{ 121 | {"md", "meta_data_t *"}, 122 | {"key", "char const *"}, 123 | {"value", "uint64_t"}, 124 | }, 125 | Ret: "int", 126 | }, 127 | { 128 | Name: "meta_data_add_string", 129 | Args: []Argument{ 130 | {"md", "meta_data_t *"}, 131 | {"key", "char const *"}, 132 | {"value", "char const *"}, 133 | }, 134 | Ret: "int", 135 | }, 136 | { 137 | Name: "meta_data_get_boolean", 138 | Args: []Argument{ 139 | {"md", "meta_data_t *"}, 140 | {"key", "char const *"}, 141 | {"value", "bool *"}, 142 | }, 143 | Ret: "int", 144 | }, 145 | { 146 | Name: "meta_data_get_double", 147 | Args: []Argument{ 148 | {"md", "meta_data_t *"}, 149 | {"key", "char const *"}, 150 | {"value", "double *"}, 151 | }, 152 | Ret: "int", 153 | }, 154 | { 155 | Name: "meta_data_get_signed_int", 156 | Args: []Argument{ 157 | {"md", "meta_data_t *"}, 158 | {"key", "char const *"}, 159 | {"value", "int64_t *"}, 160 | }, 161 | Ret: "int", 162 | }, 163 | { 164 | Name: "meta_data_get_unsigned_int", 165 | Args: []Argument{ 166 | {"md", "meta_data_t *"}, 167 | {"key", "char const *"}, 168 | {"value", "uint64_t *"}, 169 | }, 170 | Ret: "int", 171 | }, 172 | { 173 | Name: "meta_data_get_string", 174 | Args: []Argument{ 175 | {"md", "meta_data_t *"}, 176 | {"key", "char const *"}, 177 | {"value", "char **"}, 178 | }, 179 | Ret: "int", 180 | }, 181 | } 182 | 183 | const ptrTmpl = "static {{.Ret}} (*{{.Name}}_ptr)({{.ArgsTypes}});\n" 184 | 185 | const wrapperTmpl = `{{.Ret}} {{.Name}}_wrapper({{.ArgsStr}}) { 186 | LOAD({{.Name}}); 187 | {{if not .IsVoid}}return {{end}}(*{{.Name}}_ptr)({{.ArgsNames}}); 188 | } 189 | 190 | ` 191 | 192 | type Argument struct { 193 | Name string 194 | Type string 195 | } 196 | 197 | func (a Argument) String() string { 198 | return a.Type + " " + a.Name 199 | } 200 | 201 | type Function struct { 202 | Name string 203 | Args []Argument 204 | Ret string 205 | } 206 | 207 | func (f Function) ArgsTypes() string { 208 | if len(f.Args) == 0 { 209 | return "void" 210 | } 211 | 212 | var args []string 213 | for _, a := range f.Args { 214 | args = append(args, a.Type) 215 | } 216 | 217 | return strings.Join(args, ", ") 218 | } 219 | 220 | func (f Function) ArgsNames() string { 221 | var args []string 222 | for _, a := range f.Args { 223 | args = append(args, a.Name) 224 | } 225 | 226 | return strings.Join(args, ", ") 227 | } 228 | 229 | func (f Function) ArgsStr() string { 230 | if len(f.Args) == 0 { 231 | return "void" 232 | } 233 | 234 | var args []string 235 | for _, a := range f.Args { 236 | args = append(args, a.String()) 237 | } 238 | 239 | return strings.Join(args, ", ") 240 | } 241 | 242 | func (f Function) IsVoid() bool { 243 | return f.Ret == "void" 244 | } 245 | 246 | type byName []Function 247 | 248 | func (f byName) Len() int { return len(f) } 249 | func (f byName) Less(i, j int) bool { return f[i].Name < f[j].Name } 250 | func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] } 251 | 252 | func main() { 253 | var rawC bytes.Buffer 254 | fmt.Fprint(&rawC, `#include "plugin.h" 255 | #include 256 | #include 257 | #include 258 | 259 | #define LOAD(f) \ 260 | if (f##_ptr == NULL) { \ 261 | void *hnd = dlopen(NULL, RTLD_LAZY); \ 262 | f##_ptr = dlsym(hnd, #f); \ 263 | dlclose(hnd); \ 264 | } 265 | 266 | `) 267 | 268 | sort.Sort(byName(functions)) 269 | 270 | t, err := template.New("ptr").Parse(ptrTmpl) 271 | if err != nil { 272 | log.Fatal(err) 273 | } 274 | for _, f := range functions { 275 | if err := t.Execute(&rawC, f); err != nil { 276 | log.Fatal(err) 277 | } 278 | } 279 | 280 | fmt.Fprintln(&rawC) 281 | 282 | t, err = template.New("wrapper").Parse(wrapperTmpl) 283 | if err != nil { 284 | log.Fatal(err) 285 | } 286 | for _, f := range functions { 287 | if err := t.Execute(&rawC, f); err != nil { 288 | log.Fatal(err) 289 | } 290 | } 291 | 292 | var fmtC bytes.Buffer 293 | 294 | cmd := exec.Command("clang-format") 295 | cmd.Stdin = &rawC 296 | cmd.Stdout = &fmtC 297 | cmd.Stderr = os.Stderr 298 | if err := cmd.Run(); err != nil { 299 | log.Fatal(err) 300 | } 301 | 302 | fmt.Print(`// +build go1.5,cgo 303 | 304 | package plugin // import "collectd.org/plugin" 305 | 306 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 307 | // #cgo LDFLAGS: -ldl 308 | `) 309 | s := bufio.NewScanner(&fmtC) 310 | for s.Scan() { 311 | fmt.Println("//", s.Text()) 312 | } 313 | fmt.Println(`import "C"`) 314 | } 315 | -------------------------------------------------------------------------------- /api/types.go: -------------------------------------------------------------------------------- 1 | // Package api defines data types representing core collectd data types. 2 | package api // import "collectd.org/api" 3 | 4 | import ( 5 | "bufio" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "math" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var ( 17 | // ErrNoDataset is returned when the data set cannot be found. 18 | ErrNoDataset = errors.New("no such dataset") 19 | ) 20 | 21 | var ( 22 | dsTypeCounter = reflect.TypeOf(Counter(0)) 23 | dsTypeDerive = reflect.TypeOf(Derive(0)) 24 | dsTypeGauge = reflect.TypeOf(Gauge(0)) 25 | ) 26 | 27 | // TypesDB holds the type definitions of one or more types.db(5) files. 28 | type TypesDB struct { 29 | rows map[string]*DataSet 30 | } 31 | 32 | // NewTypesDB parses collectd's types.db file and returns an initialized 33 | // TypesDB object that can be queried. 34 | func NewTypesDB(r io.Reader) (*TypesDB, error) { 35 | db := &TypesDB{ 36 | rows: make(map[string]*DataSet), 37 | } 38 | 39 | s := bufio.NewScanner(r) 40 | for s.Scan() { 41 | line := s.Text() 42 | if line == "" || strings.HasPrefix(line, "#") { 43 | continue 44 | } 45 | 46 | ds, err := parseSet(s.Text()) 47 | if err != nil { 48 | continue 49 | } 50 | 51 | db.rows[ds.Name] = ds 52 | } 53 | 54 | if err := s.Err(); err != nil { 55 | return nil, err 56 | } 57 | 58 | return db, nil 59 | } 60 | 61 | // Merge adds all entries in other to db, possibly overwriting entries in db 62 | // with the same name. 63 | func (db *TypesDB) Merge(other *TypesDB) { 64 | for k, v := range other.rows { 65 | db.rows[k] = v 66 | } 67 | } 68 | 69 | // DataSet returns the DataSet "typ". 70 | // This is similar to collectd's plugin_get_ds() function. 71 | func (db *TypesDB) DataSet(typ string) (*DataSet, bool) { 72 | s, ok := db.rows[typ] 73 | return s, ok 74 | } 75 | 76 | // ValueList initializes and returns a new ValueList. The number of values 77 | // arguments must match the number of DataSources in the vl.Type DataSet and 78 | // are converted to []Value using DataSet.Values(). 79 | func (db *TypesDB) ValueList(id Identifier, t time.Time, interval time.Duration, values ...interface{}) (*ValueList, error) { 80 | ds, ok := db.DataSet(id.Type) 81 | if !ok { 82 | return nil, ErrNoDataset 83 | } 84 | 85 | v, err := ds.Values(values...) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return &ValueList{ 91 | Identifier: id, 92 | Time: t, 93 | Interval: interval, 94 | DSNames: ds.Names(), 95 | Values: v, 96 | }, nil 97 | } 98 | 99 | // DataSet defines the metrics of a given "Type", i.e. the name, kind (Derive, 100 | // Gauge, …) and minimum and maximum values. 101 | type DataSet struct { 102 | Name string 103 | Sources []DataSource 104 | } 105 | 106 | func parseSet(line string) (*DataSet, error) { 107 | ds := &DataSet{} 108 | 109 | s := bufio.NewScanner(strings.NewReader(line)) 110 | s.Split(bufio.ScanWords) 111 | 112 | for s.Scan() { 113 | if ds.Name == "" { 114 | ds.Name = s.Text() 115 | continue 116 | } 117 | 118 | dsrc, err := parseSource(s.Text()) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | ds.Sources = append(ds.Sources, *dsrc) 124 | } 125 | 126 | if err := s.Err(); err != nil { 127 | return nil, err 128 | } 129 | 130 | return ds, nil 131 | } 132 | 133 | // Names returns a slice of the data source names. This can be used to populate ValueList.DSNames. 134 | func (ds *DataSet) Names() []string { 135 | var ret []string 136 | for _, dsrc := range ds.Sources { 137 | ret = append(ret, dsrc.Name) 138 | } 139 | 140 | return ret 141 | } 142 | 143 | // Values converts the arguments to the Value interface type and returns them 144 | // as a slice. It expects the same number of arguments as it has Sources and 145 | // will return an error if there is a mismatch. Each argument is converted to a 146 | // Counter, Derive or Gauge according to the corresponding DataSource.Type. 147 | func (ds *DataSet) Values(args ...interface{}) ([]Value, error) { 148 | if len(args) != len(ds.Sources) { 149 | return nil, fmt.Errorf("len(args) = %d, want %d", len(args), len(ds.Sources)) 150 | } 151 | 152 | var ret []Value 153 | for i, arg := range args { 154 | v, err := ds.Sources[i].Value(arg) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | ret = append(ret, v) 160 | } 161 | 162 | return ret, nil 163 | } 164 | 165 | // Check does sanity checking of vl and returns an error if it finds a problem. 166 | // Sanity checking includes checking the concrete types in the Values slice 167 | // against the DataSet's Sources. 168 | func (ds *DataSet) Check(vl *ValueList) error { 169 | if ds.Name != vl.Type { 170 | return fmt.Errorf("vl.Type = %q, want %q", vl.Type, ds.Name) 171 | } 172 | 173 | if len(ds.Sources) != len(vl.Values) { 174 | return fmt.Errorf("len(vl.Values) = %d, want %d", len(vl.Values), len(ds.Sources)) 175 | } 176 | 177 | if len(ds.Sources) != len(vl.DSNames) { 178 | return fmt.Errorf("len(vl.DSNames) = %d, want %d", len(vl.DSNames), len(ds.Sources)) 179 | } 180 | 181 | for i, dsrc := range ds.Sources { 182 | if dsrc.Name != vl.DSNames[i] { 183 | return fmt.Errorf("vl.DSNames[%d] = %q, want %q", i, vl.DSNames[i], dsrc.Name) 184 | } 185 | 186 | if reflect.TypeOf(vl.Values[i]) != dsrc.Type { 187 | return fmt.Errorf("vl.Values[%d] is a %T, want %s", i, vl.Values[i], dsrc.Type.Name()) 188 | } 189 | } 190 | 191 | return nil 192 | } 193 | 194 | // DataSource defines one metric within a "Type" / DataSet. Type is one of 195 | // Counter, Derive and Gauge. Min and Max apply to the rates of Counter and 196 | // Derive types, not the raw incremental value. 197 | type DataSource struct { 198 | Name string 199 | Type reflect.Type 200 | Min, Max float64 201 | } 202 | 203 | func parseSource(line string) (*DataSource, error) { 204 | line = strings.TrimSuffix(line, ",") 205 | 206 | f := strings.Split(line, ":") 207 | if len(f) != 4 { 208 | return nil, fmt.Errorf("unexpected field count: %d", len(f)) 209 | } 210 | 211 | dsrc := &DataSource{ 212 | Name: f[0], 213 | Min: math.NaN(), 214 | Max: math.NaN(), 215 | } 216 | 217 | switch f[1] { 218 | case "COUNTER": 219 | dsrc.Type = dsTypeCounter 220 | case "DERIVE": 221 | dsrc.Type = dsTypeDerive 222 | case "GAUGE": 223 | dsrc.Type = dsTypeGauge 224 | default: 225 | return nil, fmt.Errorf("invalid data source type %q", f[1]) 226 | } 227 | 228 | if f[2] != "U" { 229 | v, err := strconv.ParseFloat(f[2], 64) 230 | if err != nil { 231 | return nil, err 232 | } 233 | dsrc.Min = v 234 | } 235 | 236 | if f[3] != "U" { 237 | v, err := strconv.ParseFloat(f[3], 64) 238 | if err != nil { 239 | return nil, err 240 | } 241 | dsrc.Max = v 242 | } 243 | 244 | return dsrc, nil 245 | } 246 | 247 | // Value converts arg to a Counter, Derive or Gauge and returns it as the Value 248 | // interface type. Returns an error if arg cannot be converted. 249 | func (dsrc DataSource) Value(arg interface{}) (Value, error) { 250 | if !reflect.TypeOf(arg).ConvertibleTo(dsrc.Type) { 251 | return nil, fmt.Errorf("cannot convert %T to %s", arg, dsrc.Type.Name()) 252 | } 253 | 254 | v := reflect.ValueOf(arg).Convert(dsrc.Type) 255 | switch dsrc.Type { 256 | case dsTypeCounter: 257 | return v.Interface().(Counter), nil 258 | case dsTypeDerive: 259 | return v.Interface().(Derive), nil 260 | case dsTypeGauge: 261 | return v.Interface().(Gauge), nil 262 | } 263 | 264 | return nil, fmt.Errorf("unexpected data sourc type %s", dsrc.Type.Name()) 265 | } 266 | -------------------------------------------------------------------------------- /plugin/wrapper.go: -------------------------------------------------------------------------------- 1 | // +build go1.5,cgo 2 | 3 | package plugin // import "collectd.org/plugin" 4 | 5 | // #cgo CPPFLAGS: -DHAVE_CONFIG_H 6 | // #cgo LDFLAGS: -ldl 7 | // #include "plugin.h" 8 | // #include 9 | // #include 10 | // #include 11 | // 12 | // #define LOAD(f) \ 13 | // if (f##_ptr == NULL) { \ 14 | // void *hnd = dlopen(NULL, RTLD_LAZY); \ 15 | // f##_ptr = dlsym(hnd, #f); \ 16 | // dlclose(hnd); \ 17 | // } 18 | // 19 | // static int (*meta_data_add_boolean_ptr)(meta_data_t *, char const *, bool); 20 | // static int (*meta_data_add_double_ptr)(meta_data_t *, char const *, double); 21 | // static int (*meta_data_add_signed_int_ptr)(meta_data_t *, char const *, 22 | // int64_t); 23 | // static int (*meta_data_add_string_ptr)(meta_data_t *, char const *, 24 | // char const *); 25 | // static int (*meta_data_add_unsigned_int_ptr)(meta_data_t *, char const *, 26 | // uint64_t); 27 | // static meta_data_t *(*meta_data_create_ptr)(void); 28 | // static void (*meta_data_destroy_ptr)(meta_data_t *); 29 | // static int (*meta_data_get_boolean_ptr)(meta_data_t *, char const *, bool *); 30 | // static int (*meta_data_get_double_ptr)(meta_data_t *, char const *, double *); 31 | // static int (*meta_data_get_signed_int_ptr)(meta_data_t *, char const *, 32 | // int64_t *); 33 | // static int (*meta_data_get_string_ptr)(meta_data_t *, char const *, char **); 34 | // static int (*meta_data_get_unsigned_int_ptr)(meta_data_t *, char const *, 35 | // uint64_t *); 36 | // static int (*meta_data_toc_ptr)(meta_data_t *, char ***); 37 | // static int (*meta_data_type_ptr)(meta_data_t *, char const *); 38 | // static int (*plugin_dispatch_values_ptr)(value_list_t const *); 39 | // static cdtime_t (*plugin_get_interval_ptr)(void); 40 | // static int (*plugin_register_complex_read_ptr)(meta_data_t *, char const *, 41 | // plugin_read_cb, cdtime_t, 42 | // user_data_t *); 43 | // static int (*plugin_register_log_ptr)(char const *, plugin_log_cb, 44 | // user_data_t *); 45 | // static int (*plugin_register_shutdown_ptr)(char const *, plugin_shutdown_cb); 46 | // static int (*plugin_register_write_ptr)(char const *, plugin_write_cb, 47 | // user_data_t *); 48 | // 49 | // int meta_data_add_boolean_wrapper(meta_data_t *md, char const *key, 50 | // bool value) { 51 | // LOAD(meta_data_add_boolean); 52 | // return (*meta_data_add_boolean_ptr)(md, key, value); 53 | // } 54 | // 55 | // int meta_data_add_double_wrapper(meta_data_t *md, char const *key, 56 | // double value) { 57 | // LOAD(meta_data_add_double); 58 | // return (*meta_data_add_double_ptr)(md, key, value); 59 | // } 60 | // 61 | // int meta_data_add_signed_int_wrapper(meta_data_t *md, char const *key, 62 | // int64_t value) { 63 | // LOAD(meta_data_add_signed_int); 64 | // return (*meta_data_add_signed_int_ptr)(md, key, value); 65 | // } 66 | // 67 | // int meta_data_add_string_wrapper(meta_data_t *md, char const *key, 68 | // char const *value) { 69 | // LOAD(meta_data_add_string); 70 | // return (*meta_data_add_string_ptr)(md, key, value); 71 | // } 72 | // 73 | // int meta_data_add_unsigned_int_wrapper(meta_data_t *md, char const *key, 74 | // uint64_t value) { 75 | // LOAD(meta_data_add_unsigned_int); 76 | // return (*meta_data_add_unsigned_int_ptr)(md, key, value); 77 | // } 78 | // 79 | // meta_data_t *meta_data_create_wrapper(void) { 80 | // LOAD(meta_data_create); 81 | // return (*meta_data_create_ptr)(); 82 | // } 83 | // 84 | // void meta_data_destroy_wrapper(meta_data_t *md) { 85 | // LOAD(meta_data_destroy); 86 | // (*meta_data_destroy_ptr)(md); 87 | // } 88 | // 89 | // int meta_data_get_boolean_wrapper(meta_data_t *md, char const *key, 90 | // bool *value) { 91 | // LOAD(meta_data_get_boolean); 92 | // return (*meta_data_get_boolean_ptr)(md, key, value); 93 | // } 94 | // 95 | // int meta_data_get_double_wrapper(meta_data_t *md, char const *key, 96 | // double *value) { 97 | // LOAD(meta_data_get_double); 98 | // return (*meta_data_get_double_ptr)(md, key, value); 99 | // } 100 | // 101 | // int meta_data_get_signed_int_wrapper(meta_data_t *md, char const *key, 102 | // int64_t *value) { 103 | // LOAD(meta_data_get_signed_int); 104 | // return (*meta_data_get_signed_int_ptr)(md, key, value); 105 | // } 106 | // 107 | // int meta_data_get_string_wrapper(meta_data_t *md, char const *key, 108 | // char **value) { 109 | // LOAD(meta_data_get_string); 110 | // return (*meta_data_get_string_ptr)(md, key, value); 111 | // } 112 | // 113 | // int meta_data_get_unsigned_int_wrapper(meta_data_t *md, char const *key, 114 | // uint64_t *value) { 115 | // LOAD(meta_data_get_unsigned_int); 116 | // return (*meta_data_get_unsigned_int_ptr)(md, key, value); 117 | // } 118 | // 119 | // int meta_data_toc_wrapper(meta_data_t *md, char ***toc) { 120 | // LOAD(meta_data_toc); 121 | // return (*meta_data_toc_ptr)(md, toc); 122 | // } 123 | // 124 | // int meta_data_type_wrapper(meta_data_t *md, char const *key) { 125 | // LOAD(meta_data_type); 126 | // return (*meta_data_type_ptr)(md, key); 127 | // } 128 | // 129 | // int plugin_dispatch_values_wrapper(value_list_t const *vl) { 130 | // LOAD(plugin_dispatch_values); 131 | // return (*plugin_dispatch_values_ptr)(vl); 132 | // } 133 | // 134 | // cdtime_t plugin_get_interval_wrapper(void) { 135 | // LOAD(plugin_get_interval); 136 | // return (*plugin_get_interval_ptr)(); 137 | // } 138 | // 139 | // int plugin_register_complex_read_wrapper(meta_data_t *group, char const *name, 140 | // plugin_read_cb callback, 141 | // cdtime_t interval, user_data_t *ud) { 142 | // LOAD(plugin_register_complex_read); 143 | // return (*plugin_register_complex_read_ptr)(group, name, callback, interval, 144 | // ud); 145 | // } 146 | // 147 | // int plugin_register_log_wrapper(char const *name, plugin_log_cb callback, 148 | // user_data_t *ud) { 149 | // LOAD(plugin_register_log); 150 | // return (*plugin_register_log_ptr)(name, callback, ud); 151 | // } 152 | // 153 | // int plugin_register_shutdown_wrapper(char const *name, 154 | // plugin_shutdown_cb callback) { 155 | // LOAD(plugin_register_shutdown); 156 | // return (*plugin_register_shutdown_ptr)(name, callback); 157 | // } 158 | // 159 | // int plugin_register_write_wrapper(char const *name, plugin_write_cb callback, 160 | // user_data_t *ud) { 161 | // LOAD(plugin_register_write); 162 | // return (*plugin_register_write_ptr)(name, callback, ud); 163 | // } 164 | import "C" 165 | -------------------------------------------------------------------------------- /api/main.go: -------------------------------------------------------------------------------- 1 | // Package api defines data types representing core collectd data types. 2 | package api // import "collectd.org/api" 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "collectd.org/meta" 14 | "go.uber.org/multierr" 15 | ) 16 | 17 | // Value represents either a Gauge or a Derive. It is Go's equivalent to the C 18 | // union value_t. If a function accepts a Value, you may pass in either a Gauge 19 | // or a Derive. Passing in any other type may or may not panic. 20 | type Value interface { 21 | Type() string 22 | } 23 | 24 | // Gauge represents a gauge metric value, such as a temperature. 25 | // This is Go's equivalent to the C type "gauge_t". 26 | type Gauge float64 27 | 28 | // Type returns "gauge". 29 | func (v Gauge) Type() string { return "gauge" } 30 | 31 | // Derive represents a counter metric value, such as bytes sent over the 32 | // network. When the counter wraps around (overflows) or is reset, this is 33 | // interpreted as a (huge) negative rate, which is discarded. 34 | // This is Go's equivalent to the C type "derive_t". 35 | type Derive int64 36 | 37 | // Type returns "derive". 38 | func (v Derive) Type() string { return "derive" } 39 | 40 | // Counter represents a counter metric value, such as bytes sent over the 41 | // network. When a counter value is smaller than the previous value, a wrap 42 | // around (overflow) is assumed. This causes huge spikes in case a counter is 43 | // reset. Only use Counter for very specific cases. If in doubt, use Derive 44 | // instead. 45 | // This is Go's equivalent to the C type "counter_t". 46 | type Counter uint64 47 | 48 | // Type returns "counter". 49 | func (v Counter) Type() string { return "counter" } 50 | 51 | // Identifier identifies one metric. 52 | type Identifier struct { 53 | Host string 54 | Plugin, PluginInstance string 55 | Type, TypeInstance string 56 | } 57 | 58 | // ParseIdentifier parses the identifier encoded in s and returns it. 59 | func ParseIdentifier(s string) (Identifier, error) { 60 | fields := strings.Split(s, "/") 61 | if len(fields) != 3 { 62 | return Identifier{}, fmt.Errorf("not a valid identifier: %q", s) 63 | } 64 | 65 | id := Identifier{ 66 | Host: fields[0], 67 | Plugin: fields[1], 68 | Type: fields[2], 69 | } 70 | 71 | if i := strings.Index(id.Plugin, "-"); i != -1 { 72 | id.PluginInstance = id.Plugin[i+1:] 73 | id.Plugin = id.Plugin[:i] 74 | } 75 | 76 | if i := strings.Index(id.Type, "-"); i != -1 { 77 | id.TypeInstance = id.Type[i+1:] 78 | id.Type = id.Type[:i] 79 | } 80 | 81 | return id, nil 82 | } 83 | 84 | // ValueList represents one (set of) data point(s) of one metric. It is Go's 85 | // equivalent of the C type value_list_t. 86 | type ValueList struct { 87 | Identifier 88 | Time time.Time 89 | Interval time.Duration 90 | Values []Value 91 | DSNames []string 92 | Meta meta.Data 93 | } 94 | 95 | // DSName returns the name of the data source at the given index. If vl.DSNames 96 | // is nil, returns "value" if there is a single value and a string 97 | // representation of index otherwise. 98 | func (vl *ValueList) DSName(index int) string { 99 | if vl.DSNames != nil { 100 | return vl.DSNames[index] 101 | } else if len(vl.Values) != 1 { 102 | return strconv.FormatInt(int64(index), 10) 103 | } 104 | 105 | return "value" 106 | } 107 | 108 | // Check does a sanity check on vl and returns any errors it finds. 109 | func (vl *ValueList) Check() error { 110 | var err error 111 | 112 | if vl.Host == "" { 113 | err = multierr.Append(err, errors.New("Host is unset")) 114 | } 115 | if vl.Plugin == "" { 116 | err = multierr.Append(err, errors.New("Plugin is unset")) 117 | } 118 | if strings.ContainsRune(vl.Plugin, '-') { 119 | err = multierr.Append(err, errors.New("Plugin contains '-'")) 120 | } 121 | if vl.Type == "" { 122 | err = multierr.Append(err, errors.New("Type is unset")) 123 | } 124 | if strings.ContainsRune(vl.Type, '-') { 125 | err = multierr.Append(err, errors.New("Type contains '-'")) 126 | } 127 | if vl.Interval == 0 { 128 | err = multierr.Append(err, errors.New("Interval is unset")) 129 | } 130 | if len(vl.Values) == 0 { 131 | err = multierr.Append(err, errors.New("Values is unset")) 132 | } 133 | if n, v := len(vl.DSNames), len(vl.Values); n != 0 && v != 0 && n != v { 134 | err = multierr.Append(err, fmt.Errorf("number of values (%d) and number of DS names (%d) don't match", v, n)) 135 | } 136 | 137 | nameCount := make(map[string]int) 138 | for _, name := range vl.DSNames { 139 | nameCount[name] = nameCount[name] + 1 140 | } 141 | for name, count := range nameCount { 142 | if count != 1 { 143 | err = multierr.Append(err, fmt.Errorf("data source name %q is not unique", name)) 144 | } 145 | } 146 | 147 | return err 148 | } 149 | 150 | // Clone returns a copy of vl. 151 | // Unfortunately, many functions expect a pointer to a value list. If the 152 | // original value list must not be modified, it may be necessary to create and 153 | // pass a copy. This is what this method helps to do. 154 | func (vl *ValueList) Clone() *ValueList { 155 | if vl == nil { 156 | return nil 157 | } 158 | 159 | vlCopy := *vl 160 | 161 | vlCopy.Values = make([]Value, len(vl.Values)) 162 | copy(vlCopy.Values, vl.Values) 163 | 164 | vlCopy.DSNames = make([]string, len(vl.DSNames)) 165 | copy(vlCopy.DSNames, vl.DSNames) 166 | 167 | vlCopy.Meta = vl.Meta.Clone() 168 | 169 | return &vlCopy 170 | } 171 | 172 | // Writer are objects accepting a ValueList for writing, for example to the 173 | // network. 174 | type Writer interface { 175 | Write(context.Context, *ValueList) error 176 | } 177 | 178 | // WriterFunc implements the Writer interface based on a wrapped function. 179 | type WriterFunc func(context.Context, *ValueList) error 180 | 181 | // Write calls the wrapped function. 182 | func (f WriterFunc) Write(ctx context.Context, vl *ValueList) error { 183 | return f(ctx, vl) 184 | } 185 | 186 | // String returns a string representation of the Identifier. 187 | func (id Identifier) String() string { 188 | str := id.Host + "/" + id.Plugin 189 | if id.PluginInstance != "" { 190 | str += "-" + id.PluginInstance 191 | } 192 | str += "/" + id.Type 193 | if id.TypeInstance != "" { 194 | str += "-" + id.TypeInstance 195 | } 196 | return str 197 | } 198 | 199 | // Fanout implements a multiplexer for Writer, i.e. each ValueList written to 200 | // it is copied and written to each Writer. 201 | type Fanout []Writer 202 | 203 | // Write writes the value list to each writer. Each writer receives a copy of 204 | // the value list to avoid writers interfering with one another. Writers are 205 | // executed concurrently. Write blocks until all writers have returned and 206 | // returns an error containing all errors returned by writers. 207 | // 208 | // If the context is canceled, Write returns an error immediately. Since it may 209 | // return before all writers have finished, the returned error may not contain 210 | // the error of all writers. 211 | func (f Fanout) Write(ctx context.Context, vl *ValueList) error { 212 | var ( 213 | ch = make(chan error) 214 | wg sync.WaitGroup 215 | ) 216 | 217 | for _, w := range f { 218 | wg.Add(1) 219 | go func(w Writer) { 220 | defer wg.Done() 221 | 222 | if err := w.Write(ctx, vl.Clone()); err != nil { 223 | // block until the error is read, or until the 224 | // context is canceled. 225 | select { 226 | case ch <- fmt.Errorf("%T.Write(): %w", w, err): 227 | case <-ctx.Done(): 228 | } 229 | } 230 | }(w) 231 | } 232 | 233 | go func() { 234 | wg.Wait() 235 | close(ch) 236 | }() 237 | 238 | var errs error 239 | for { 240 | select { 241 | case err, ok := <-ch: 242 | if !ok { 243 | // channel closed, all goroutines done 244 | return errs 245 | } 246 | errs = multierr.Append(errs, err) 247 | case <-ctx.Done(): 248 | return multierr.Append(errs, ctx.Err()) 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /network/parse.go: -------------------------------------------------------------------------------- 1 | package network // import "collectd.org/network" 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "time" 10 | 11 | "collectd.org/api" 12 | "collectd.org/cdtime" 13 | ) 14 | 15 | // ErrInvalid is returned when parsing the network data was aborted due to 16 | // illegal data format. 17 | var ErrInvalid = errors.New("invalid data") 18 | 19 | // ParseOpts holds confiruation options for "Parse()". 20 | type ParseOpts struct { 21 | // PasswordLookup is used lookup passwords to verify signed data and 22 | // decrypt encrypted data. 23 | PasswordLookup PasswordLookup 24 | // SecurityLevel determines the minimum security level expected by the 25 | // caller. If set to "Sign", only signed and encrypted data is returned 26 | // by Parse(), if set to "Encrypt", only encrypted data is returned. 27 | SecurityLevel SecurityLevel 28 | // TypesDB for looking up DS names and verify data source types. 29 | TypesDB *api.TypesDB 30 | } 31 | 32 | // Parse parses the binary network format and returns a slice of ValueLists. If 33 | // a parse error is encountered, all ValueLists parsed to this point are 34 | // returned as well as the error. Unknown "parts" are silently ignored. 35 | func Parse(b []byte, opts ParseOpts) ([]*api.ValueList, error) { 36 | return parse(b, None, opts) 37 | } 38 | 39 | func readUint16(buf *bytes.Buffer) (uint16, error) { 40 | read := buf.Next(2) 41 | if len(read) != 2 { 42 | return 0, ErrInvalid 43 | } 44 | return binary.BigEndian.Uint16(read), nil 45 | } 46 | 47 | func parse(b []byte, sl SecurityLevel, opts ParseOpts) ([]*api.ValueList, error) { 48 | var valueLists []*api.ValueList 49 | 50 | var state api.ValueList 51 | buf := bytes.NewBuffer(b) 52 | 53 | for buf.Len() > 0 { 54 | partType, err := readUint16(buf) 55 | if err != nil { 56 | return nil, ErrInvalid 57 | } 58 | partLengthUnsigned, err := readUint16(buf) 59 | if err != nil { 60 | return nil, ErrInvalid 61 | } 62 | partLength := int(partLengthUnsigned) 63 | 64 | if partLength < 5 || partLength-4 > buf.Len() { 65 | return valueLists, fmt.Errorf("invalid length %d", partLength) 66 | } 67 | 68 | // First 4 bytes were already read 69 | partLength -= 4 70 | 71 | payload := buf.Next(partLength) 72 | if len(payload) != partLength { 73 | return valueLists, fmt.Errorf("invalid length: want %d, got %d", partLength, len(payload)) 74 | } 75 | 76 | switch partType { 77 | case typeHost, typePlugin, typePluginInstance, typeType, typeTypeInstance: 78 | if err := parseIdentifier(partType, payload, &state); err != nil { 79 | return valueLists, err 80 | } 81 | 82 | case typeInterval, typeIntervalHR, typeTime, typeTimeHR: 83 | if err := parseTime(partType, payload, &state); err != nil { 84 | return valueLists, err 85 | } 86 | 87 | case typeValues: 88 | v, err := parseValues(payload) 89 | if err != nil { 90 | return valueLists, err 91 | } 92 | 93 | vl := state 94 | vl.Values = v 95 | 96 | if opts.TypesDB != nil { 97 | ds, ok := opts.TypesDB.DataSet(state.Type) 98 | if !ok { 99 | log.Printf("unable to find %q in TypesDB", state.Type) 100 | continue 101 | } 102 | 103 | // convert []api.Value to []interface{} 104 | ifValues := make([]interface{}, len(vl.Values)) 105 | for i, v := range vl.Values { 106 | ifValues[i] = v 107 | } 108 | 109 | // cast all values to the correct data source type. 110 | // Returns an error if the number of values is incorrect. 111 | v, err := ds.Values(ifValues...) 112 | if err != nil { 113 | log.Printf("unable to convert metric %q, values %v according to %v in TypesDB: %v", state, ifValues, ds, err) 114 | continue 115 | } 116 | vl.Values = v 117 | vl.DSNames = ds.Names() 118 | } 119 | 120 | if opts.SecurityLevel <= sl { 121 | valueLists = append(valueLists, &vl) 122 | } 123 | 124 | case typeSignSHA256: 125 | vls, err := parseSignSHA256(payload, buf.Bytes(), opts) 126 | if err != nil { 127 | return valueLists, err 128 | } 129 | valueLists = append(valueLists, vls...) 130 | 131 | case typeEncryptAES256: 132 | vls, err := parseEncryptAES256(payload, opts) 133 | if err != nil { 134 | return valueLists, err 135 | } 136 | valueLists = append(valueLists, vls...) 137 | 138 | default: 139 | log.Printf("ignoring field of type %#x", partType) 140 | } 141 | } 142 | 143 | return valueLists, nil 144 | } 145 | 146 | func parseIdentifier(partType uint16, payload []byte, state *api.ValueList) error { 147 | str, err := parseString(payload) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | switch partType { 153 | case typeHost: 154 | state.Identifier.Host = str 155 | case typePlugin: 156 | state.Identifier.Plugin = str 157 | case typePluginInstance: 158 | state.Identifier.PluginInstance = str 159 | case typeType: 160 | state.Identifier.Type = str 161 | case typeTypeInstance: 162 | state.Identifier.TypeInstance = str 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func parseTime(partType uint16, payload []byte, state *api.ValueList) error { 169 | v, err := parseInt(payload) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | switch partType { 175 | case typeInterval: 176 | state.Interval = time.Duration(v) * time.Second 177 | case typeIntervalHR: 178 | state.Interval = cdtime.Time(v).Duration() 179 | case typeTime: 180 | state.Time = time.Unix(int64(v), 0) 181 | case typeTimeHR: 182 | state.Time = cdtime.Time(v).Time() 183 | } 184 | 185 | return nil 186 | } 187 | 188 | func parseValues(b []byte) ([]api.Value, error) { 189 | buffer := bytes.NewBuffer(b) 190 | 191 | var n uint16 192 | if err := binary.Read(buffer, binary.BigEndian, &n); err != nil { 193 | return nil, err 194 | } 195 | 196 | if int(n*9) != buffer.Len() { 197 | return nil, ErrInvalid 198 | } 199 | 200 | types := make([]byte, n) 201 | values := make([]api.Value, n) 202 | 203 | if _, err := buffer.Read(types); err != nil { 204 | return nil, err 205 | } 206 | 207 | for i, typ := range types { 208 | switch typ { 209 | case dsTypeGauge: 210 | var v float64 211 | if err := binary.Read(buffer, binary.LittleEndian, &v); err != nil { 212 | return nil, err 213 | } 214 | values[i] = api.Gauge(v) 215 | 216 | case dsTypeDerive: 217 | var v int64 218 | if err := binary.Read(buffer, binary.BigEndian, &v); err != nil { 219 | return nil, err 220 | } 221 | values[i] = api.Derive(v) 222 | 223 | case dsTypeCounter: 224 | var v uint64 225 | if err := binary.Read(buffer, binary.BigEndian, &v); err != nil { 226 | return nil, err 227 | } 228 | values[i] = api.Counter(v) 229 | 230 | default: 231 | return nil, ErrInvalid 232 | } 233 | } 234 | 235 | return values, nil 236 | } 237 | 238 | func parseSignSHA256(pkg, payload []byte, opts ParseOpts) ([]*api.ValueList, error) { 239 | ok, err := verifySHA256(pkg, payload, opts.PasswordLookup) 240 | if err != nil { 241 | return nil, err 242 | } else if !ok { 243 | return nil, errors.New("SHA256 verification failure") 244 | } 245 | 246 | return parse(payload, Sign, opts) 247 | } 248 | 249 | func parseEncryptAES256(payload []byte, opts ParseOpts) ([]*api.ValueList, error) { 250 | plaintext, err := decryptAES256(payload, opts.PasswordLookup) 251 | if err != nil { 252 | return nil, errors.New("AES256 decryption failure") 253 | } 254 | 255 | return parse(plaintext, Encrypt, opts) 256 | } 257 | 258 | func parseInt(b []byte) (uint64, error) { 259 | if len(b) != 8 { 260 | return 0, ErrInvalid 261 | } 262 | 263 | var i uint64 264 | buf := bytes.NewBuffer(b) 265 | if err := binary.Read(buf, binary.BigEndian, &i); err != nil { 266 | return 0, err 267 | } 268 | 269 | return i, nil 270 | } 271 | 272 | func parseString(b []byte) (string, error) { 273 | if b[len(b)-1] != 0 { 274 | return "", ErrInvalid 275 | } 276 | 277 | buf := bytes.NewBuffer(b[:len(b)-1]) 278 | return buf.String(), nil 279 | } 280 | -------------------------------------------------------------------------------- /api/main_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "collectd.org/api" 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestParseIdentifier(t *testing.T) { 15 | cases := []struct { 16 | Input string 17 | Want api.Identifier 18 | }{ 19 | { 20 | Input: "example.com/golang/gauge", 21 | Want: api.Identifier{ 22 | Host: "example.com", 23 | Plugin: "golang", 24 | Type: "gauge", 25 | }, 26 | }, 27 | { 28 | Input: "example.com/golang-foo/gauge-bar", 29 | Want: api.Identifier{ 30 | Host: "example.com", 31 | Plugin: "golang", 32 | PluginInstance: "foo", 33 | Type: "gauge", 34 | TypeInstance: "bar", 35 | }, 36 | }, 37 | { 38 | Input: "example.com/golang-a-b/gauge-b-c", 39 | Want: api.Identifier{ 40 | Host: "example.com", 41 | Plugin: "golang", 42 | PluginInstance: "a-b", 43 | Type: "gauge", 44 | TypeInstance: "b-c", 45 | }, 46 | }, 47 | } 48 | 49 | for i, c := range cases { 50 | if got, err := api.ParseIdentifier(c.Input); got != c.Want || err != nil { 51 | t.Errorf("case %d: got (%v, %v), want (%v, %v)", i, got, err, c.Want, nil) 52 | } 53 | } 54 | 55 | failures := []string{ 56 | "example.com/golang", 57 | "example.com/golang/gauge/extra", 58 | } 59 | 60 | for _, c := range failures { 61 | if got, err := api.ParseIdentifier(c); err == nil { 62 | t.Errorf("got (%v, %v), want (%v, !%v)", got, err, api.Identifier{}, nil) 63 | } 64 | } 65 | } 66 | 67 | func TestIdentifierString(t *testing.T) { 68 | id := api.Identifier{ 69 | Host: "example.com", 70 | Plugin: "golang", 71 | Type: "gauge", 72 | } 73 | 74 | cases := []struct { 75 | PluginInstance, TypeInstance string 76 | Want string 77 | }{ 78 | {"", "", "example.com/golang/gauge"}, 79 | {"foo", "", "example.com/golang-foo/gauge"}, 80 | {"", "foo", "example.com/golang/gauge-foo"}, 81 | {"foo", "bar", "example.com/golang-foo/gauge-bar"}, 82 | } 83 | 84 | for _, c := range cases { 85 | id.PluginInstance = c.PluginInstance 86 | id.TypeInstance = c.TypeInstance 87 | 88 | got := id.String() 89 | if got != c.Want { 90 | t.Errorf("got %q, want %q", got, c.Want) 91 | } 92 | } 93 | } 94 | 95 | type testWriter struct { 96 | got *api.ValueList 97 | wg *sync.WaitGroup 98 | ch chan struct{} 99 | err error 100 | } 101 | 102 | func (w *testWriter) Write(ctx context.Context, vl *api.ValueList) error { 103 | w.got = vl 104 | w.wg.Done() 105 | 106 | select { 107 | case <-w.ch: 108 | return w.err 109 | case <-ctx.Done(): 110 | return ctx.Err() 111 | } 112 | } 113 | 114 | type testError struct{} 115 | 116 | func (testError) Error() string { 117 | return "test error" 118 | } 119 | 120 | func TestFanout(t *testing.T) { 121 | cases := []struct { 122 | title string 123 | returnError bool 124 | cancelContext bool 125 | }{ 126 | { 127 | title: "success", 128 | }, 129 | { 130 | title: "error", 131 | returnError: true, 132 | }, 133 | { 134 | title: "context canceled", 135 | cancelContext: true, 136 | }, 137 | } 138 | 139 | for _, tc := range cases { 140 | t.Run(tc.title, func(t *testing.T) { 141 | ctx, cancel := context.WithCancel(context.Background()) 142 | defer cancel() 143 | 144 | var ( 145 | done = make(chan struct{}) 146 | wg sync.WaitGroup 147 | ) 148 | 149 | var writerError error 150 | if tc.returnError { 151 | writerError = testError{} 152 | } 153 | writers := []*testWriter{ 154 | { 155 | wg: &wg, 156 | ch: done, 157 | err: writerError, 158 | }, 159 | { 160 | wg: &wg, 161 | ch: done, 162 | err: writerError, 163 | }, 164 | } 165 | wg.Add(len(writers)) 166 | 167 | go func() { 168 | // wait for all writers to be called, then signal them to return 169 | wg.Wait() 170 | 171 | if tc.cancelContext { 172 | cancel() 173 | } else { 174 | close(done) 175 | } 176 | }() 177 | 178 | want := &api.ValueList{ 179 | Identifier: api.Identifier{ 180 | Host: "example.com", 181 | Plugin: "TestFanout", 182 | Type: "gauge", 183 | }, 184 | Values: []api.Value{api.Gauge(42)}, 185 | DSNames: []string{"value"}, 186 | } 187 | 188 | var f api.Fanout 189 | for _, w := range writers { 190 | f = append(f, w) 191 | } 192 | 193 | err := f.Write(ctx, want) 194 | switch { 195 | case tc.returnError && !errors.Is(err, testError{}): 196 | t.Errorf("Fanout.Write() = %v, want %T", err, testError{}) 197 | case tc.cancelContext && !errors.Is(err, context.Canceled): 198 | t.Errorf("Fanout.Write() = %v, want %T", err, context.Canceled) 199 | case !tc.returnError && !tc.cancelContext && err != nil: 200 | t.Errorf("Fanout.Write() = %v", err) 201 | } 202 | 203 | for i, w := range writers { 204 | if want == w.got { 205 | t.Errorf("writers[%d].vl == w.got, want copy", i) 206 | } 207 | if diff := cmp.Diff(want, w.got); diff != "" { 208 | t.Errorf("writers[%d].vl differs (+got/-want):\n%s", i, diff) 209 | } 210 | } 211 | }) 212 | } 213 | } 214 | 215 | func TestValueList_Check(t *testing.T) { 216 | baseVL := api.ValueList{ 217 | Identifier: api.Identifier{ 218 | Host: "example.com", 219 | Plugin: "TestValueList_Check", 220 | Type: "gauge", 221 | }, 222 | Time: time.Unix(1589283551, 0), 223 | Interval: 10 * time.Second, 224 | Values: []api.Value{api.Gauge(42)}, 225 | DSNames: []string{"value"}, 226 | } 227 | 228 | cases := []struct { 229 | title string 230 | modify func(vl *api.ValueList) 231 | wantErr bool 232 | }{ 233 | { 234 | title: "success", 235 | }, 236 | { 237 | title: "without host", 238 | modify: func(vl *api.ValueList) { 239 | vl.Host = "" 240 | }, 241 | wantErr: true, 242 | }, 243 | { 244 | title: "host contains hyphen", 245 | modify: func(vl *api.ValueList) { 246 | vl.Host = "example-host.com" 247 | }, 248 | }, 249 | { 250 | title: "without plugin", 251 | modify: func(vl *api.ValueList) { 252 | vl.Plugin = "" 253 | }, 254 | wantErr: true, 255 | }, 256 | { 257 | title: "plugin contains hyphen", 258 | modify: func(vl *api.ValueList) { 259 | vl.Plugin = "TestValueList-Check" 260 | }, 261 | wantErr: true, 262 | }, 263 | { 264 | title: "without type", 265 | modify: func(vl *api.ValueList) { 266 | vl.Type = "" 267 | }, 268 | wantErr: true, 269 | }, 270 | { 271 | title: "type contains hyphen", 272 | modify: func(vl *api.ValueList) { 273 | vl.Type = "http-request" 274 | }, 275 | wantErr: true, 276 | }, 277 | { 278 | title: "without time", 279 | modify: func(vl *api.ValueList) { 280 | vl.Time = time.Time{} 281 | }, 282 | }, 283 | { 284 | title: "without interval", 285 | modify: func(vl *api.ValueList) { 286 | vl.Interval = 0 287 | }, 288 | wantErr: true, 289 | }, 290 | { 291 | title: "without values", 292 | modify: func(vl *api.ValueList) { 293 | vl.Values = nil 294 | }, 295 | wantErr: true, 296 | }, 297 | { 298 | title: "surplus values", 299 | modify: func(vl *api.ValueList) { 300 | vl.Values = []api.Value{api.Gauge(1), api.Gauge(2)} 301 | }, 302 | wantErr: true, 303 | }, 304 | { 305 | title: "without dsnames", 306 | modify: func(vl *api.ValueList) { 307 | vl.DSNames = nil 308 | }, 309 | }, 310 | { 311 | title: "surplus dsnames", 312 | modify: func(vl *api.ValueList) { 313 | vl.DSNames = []string{"rx", "tx"} 314 | }, 315 | wantErr: true, 316 | }, 317 | { 318 | title: "multiple values", 319 | modify: func(vl *api.ValueList) { 320 | vl.Type = "if_octets" 321 | vl.Values = []api.Value{api.Derive(0), api.Derive(0)} 322 | vl.DSNames = []string{"rx", "tx"} 323 | }, 324 | }, 325 | { 326 | title: "ds name not unique", 327 | modify: func(vl *api.ValueList) { 328 | vl.Type = "if_octets" 329 | vl.Values = []api.Value{api.Derive(0), api.Derive(0)} 330 | vl.DSNames = []string{"value", "value"} 331 | }, 332 | wantErr: true, 333 | }, 334 | } 335 | 336 | for _, tc := range cases { 337 | t.Run(tc.title, func(t *testing.T) { 338 | vl := baseVL.Clone() 339 | if tc.modify != nil { 340 | tc.modify(vl) 341 | } 342 | 343 | err := vl.Check() 344 | if gotErr := err != nil; gotErr != tc.wantErr { 345 | t.Errorf("%#v.Check() = %v, want error %v", vl, err, tc.wantErr) 346 | 347 | } 348 | }) 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /network/buffer.go: -------------------------------------------------------------------------------- 1 | package network // import "collectd.org/network" 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "errors" 8 | "io" 9 | "math" 10 | "sync" 11 | "time" 12 | 13 | "collectd.org/api" 14 | "collectd.org/cdtime" 15 | ) 16 | 17 | // ErrNotEnoughSpace is returned when adding a ValueList would exeed the buffer 18 | // size. 19 | var ErrNotEnoughSpace = errors.New("not enough space") 20 | 21 | // ErrUnknownType is returned when attempting to write values of an unknown type 22 | var ErrUnknownType = errors.New("unknown type") 23 | 24 | // Buffer contains the binary representation of multiple ValueLists and state 25 | // optimally write the next ValueList. 26 | type Buffer struct { 27 | lock *sync.Mutex 28 | buffer *bytes.Buffer 29 | output io.Writer 30 | state api.ValueList 31 | size int 32 | username, password string 33 | securityLevel SecurityLevel 34 | } 35 | 36 | // NewBuffer initializes a new Buffer. If "size" is 0, DefaultBufferSize will 37 | // be used. 38 | func NewBuffer(size int) *Buffer { 39 | if size <= 0 { 40 | size = DefaultBufferSize 41 | } 42 | 43 | return &Buffer{ 44 | lock: new(sync.Mutex), 45 | buffer: new(bytes.Buffer), 46 | size: size, 47 | } 48 | } 49 | 50 | // Sign enables cryptographic signing of data. 51 | func (b *Buffer) Sign(username, password string) { 52 | b.username = username 53 | b.password = password 54 | b.securityLevel = Sign 55 | } 56 | 57 | // Encrypt enables encryption of data. 58 | func (b *Buffer) Encrypt(username, password string) { 59 | b.username = username 60 | b.password = password 61 | b.securityLevel = Encrypt 62 | } 63 | 64 | // Available returns the number of bytes still available in the buffer. 65 | func (b *Buffer) Available() int { 66 | var overhead int 67 | switch b.securityLevel { 68 | case Sign: 69 | overhead = 36 + len(b.username) 70 | case Encrypt: 71 | overhead = 42 + len(b.username) 72 | } 73 | 74 | unavail := overhead + b.buffer.Len() 75 | if b.size < unavail { 76 | return 0 77 | } 78 | return b.size - unavail 79 | } 80 | 81 | // Bytes returns the content of the buffer as a byte slice. 82 | // If signing or encrypting are enabled, the content will be signed / encrypted 83 | // prior to being returned. 84 | // This method resets the buffer. 85 | func (b *Buffer) Bytes() ([]byte, error) { 86 | tmp := make([]byte, b.size) 87 | 88 | n, err := b.Read(tmp) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | return tmp[:n], nil 94 | } 95 | 96 | // Read reads the buffer into "out". If signing or encryption is enabled, data 97 | // will be signed / encrypted before writing it to "out". Returns 98 | // ErrNotEnoughSpace if the provided buffer is too small to hold the entire 99 | // packet data. 100 | func (b *Buffer) Read(out []byte) (int, error) { 101 | b.lock.Lock() 102 | defer b.lock.Unlock() 103 | 104 | switch b.securityLevel { 105 | case Sign: 106 | return b.readSigned(out) 107 | case Encrypt: 108 | return b.readEncrypted(out) 109 | } 110 | 111 | if len(out) < b.buffer.Len() { 112 | return 0, ErrNotEnoughSpace 113 | } 114 | 115 | n := copy(out, b.buffer.Bytes()) 116 | 117 | b.reset() 118 | return n, nil 119 | } 120 | 121 | func (b *Buffer) readSigned(out []byte) (int, error) { 122 | if len(out) < 36+len(b.username)+b.buffer.Len() { 123 | return 0, ErrNotEnoughSpace 124 | } 125 | 126 | signed := signSHA256(b.buffer.Bytes(), b.username, b.password) 127 | 128 | b.reset() 129 | return copy(out, signed), nil 130 | } 131 | 132 | func (b *Buffer) readEncrypted(out []byte) (int, error) { 133 | if len(out) < 42+len(b.username)+b.buffer.Len() { 134 | return 0, ErrNotEnoughSpace 135 | } 136 | 137 | ciphertext, err := encryptAES256(b.buffer.Bytes(), b.username, b.password) 138 | if err != nil { 139 | return 0, err 140 | } 141 | 142 | b.reset() 143 | return copy(out, ciphertext), nil 144 | } 145 | 146 | // WriteTo writes the buffer contents to "w". It implements the io.WriteTo 147 | // interface. 148 | func (b *Buffer) WriteTo(w io.Writer) (int64, error) { 149 | tmp := make([]byte, b.size) 150 | 151 | n, err := b.Read(tmp) 152 | if err != nil { 153 | return 0, err 154 | } 155 | 156 | n, err = w.Write(tmp[:n]) 157 | return int64(n), err 158 | } 159 | 160 | // Write adds a ValueList to the buffer. Returns ErrNotEnoughSpace if not 161 | // enough space in the buffer is available to add this value list. In that 162 | // case, call Read() to empty the buffer and try again. 163 | func (b *Buffer) Write(_ context.Context, vl *api.ValueList) error { 164 | b.lock.Lock() 165 | defer b.lock.Unlock() 166 | 167 | // remember the original buffer size so we can truncate all potentially 168 | // written data in case of an error. 169 | l := b.buffer.Len() 170 | 171 | if err := b.writeValueList(vl); err != nil { 172 | if l != 0 { 173 | b.buffer.Truncate(l) 174 | } 175 | return err 176 | } 177 | return nil 178 | } 179 | 180 | func (b *Buffer) writeValueList(vl *api.ValueList) error { 181 | if err := b.writeIdentifier(vl.Identifier); err != nil { 182 | return err 183 | } 184 | 185 | if err := b.writeTime(vl.Time); err != nil { 186 | return err 187 | } 188 | 189 | if err := b.writeInterval(vl.Interval); err != nil { 190 | return err 191 | } 192 | 193 | if err := b.writeValues(vl.Values); err != nil { 194 | return err 195 | } 196 | 197 | return nil 198 | } 199 | 200 | func (b *Buffer) writeIdentifier(id api.Identifier) error { 201 | if id.Host != b.state.Host { 202 | if err := b.writeString(typeHost, id.Host); err != nil { 203 | return err 204 | } 205 | b.state.Host = id.Host 206 | } 207 | if id.Plugin != b.state.Plugin { 208 | if err := b.writeString(typePlugin, id.Plugin); err != nil { 209 | return err 210 | } 211 | b.state.Plugin = id.Plugin 212 | } 213 | if id.PluginInstance != b.state.PluginInstance { 214 | if err := b.writeString(typePluginInstance, id.PluginInstance); err != nil { 215 | return err 216 | } 217 | b.state.PluginInstance = id.PluginInstance 218 | } 219 | if id.Type != b.state.Type { 220 | if err := b.writeString(typeType, id.Type); err != nil { 221 | return err 222 | } 223 | b.state.Type = id.Type 224 | } 225 | if id.TypeInstance != b.state.TypeInstance { 226 | if err := b.writeString(typeTypeInstance, id.TypeInstance); err != nil { 227 | return err 228 | } 229 | b.state.TypeInstance = id.TypeInstance 230 | } 231 | 232 | return nil 233 | } 234 | 235 | func (b *Buffer) writeTime(t time.Time) error { 236 | if b.state.Time == t { 237 | return nil 238 | } 239 | b.state.Time = t 240 | 241 | return b.writeInt(typeTimeHR, uint64(cdtime.New(t))) 242 | } 243 | 244 | func (b *Buffer) writeInterval(d time.Duration) error { 245 | if b.state.Interval == d { 246 | return nil 247 | } 248 | b.state.Interval = d 249 | 250 | return b.writeInt(typeIntervalHR, uint64(cdtime.NewDuration(d))) 251 | } 252 | 253 | func (b *Buffer) writeValues(values []api.Value) error { 254 | size := 6 + 9*len(values) 255 | if size > b.Available() { 256 | return ErrNotEnoughSpace 257 | } 258 | 259 | binary.Write(b.buffer, binary.BigEndian, uint16(typeValues)) 260 | binary.Write(b.buffer, binary.BigEndian, uint16(size)) 261 | binary.Write(b.buffer, binary.BigEndian, uint16(len(values))) 262 | 263 | for _, v := range values { 264 | switch v.(type) { 265 | case api.Gauge: 266 | binary.Write(b.buffer, binary.BigEndian, uint8(dsTypeGauge)) 267 | case api.Derive: 268 | binary.Write(b.buffer, binary.BigEndian, uint8(dsTypeDerive)) 269 | case api.Counter: 270 | binary.Write(b.buffer, binary.BigEndian, uint8(dsTypeCounter)) 271 | default: 272 | return ErrUnknownType 273 | } 274 | } 275 | 276 | for _, v := range values { 277 | switch v := v.(type) { 278 | case api.Gauge: 279 | if math.IsNaN(float64(v)) { 280 | b.buffer.Write([]byte{0, 0, 0, 0, 0, 0, 0xf8, 0x7f}) 281 | } else { 282 | // sic: floats are encoded in little endian. 283 | binary.Write(b.buffer, binary.LittleEndian, float64(v)) 284 | } 285 | case api.Derive: 286 | binary.Write(b.buffer, binary.BigEndian, int64(v)) 287 | case api.Counter: 288 | binary.Write(b.buffer, binary.BigEndian, uint64(v)) 289 | default: 290 | return ErrUnknownType 291 | } 292 | } 293 | 294 | return nil 295 | } 296 | 297 | func (b *Buffer) writeString(typ uint16, s string) error { 298 | encoded := bytes.NewBufferString(s) 299 | encoded.Write([]byte{0}) 300 | 301 | // Because s is a Unicode string, encoded.Len() may be larger than 302 | // len(s). 303 | size := 4 + encoded.Len() 304 | if size > b.Available() { 305 | return ErrNotEnoughSpace 306 | } 307 | 308 | binary.Write(b.buffer, binary.BigEndian, typ) 309 | binary.Write(b.buffer, binary.BigEndian, uint16(size)) 310 | b.buffer.Write(encoded.Bytes()) 311 | 312 | return nil 313 | } 314 | 315 | func (b *Buffer) writeInt(typ uint16, n uint64) error { 316 | size := 12 317 | if size > b.Available() { 318 | return ErrNotEnoughSpace 319 | } 320 | 321 | binary.Write(b.buffer, binary.BigEndian, typ) 322 | binary.Write(b.buffer, binary.BigEndian, uint16(size)) 323 | binary.Write(b.buffer, binary.BigEndian, n) 324 | 325 | return nil 326 | } 327 | 328 | func (b *Buffer) reset() { 329 | b.buffer.Reset() 330 | b.state = api.ValueList{} 331 | } 332 | 333 | /* 334 | func (b *Buffer) flush() error { 335 | if b.buffer.Len() == 0 { 336 | return nil 337 | } 338 | 339 | buf := make([]byte, b.buffer.Len()) 340 | if _, err := b.buffer.Read(buf); err != nil { 341 | return err 342 | } 343 | 344 | if b.username != "" && b.password != "" { 345 | if b.encrypt { 346 | var err error 347 | if buf, err = encryptAES256(buf, b.username, b.password); err != nil { 348 | return err 349 | } 350 | } else { 351 | buf = signSHA256(buf, b.username, b.password) 352 | } 353 | } 354 | 355 | if _, err := b.output.Write(buf); err != nil { 356 | return err 357 | } 358 | 359 | // zero state 360 | b.state = api.ValueList{} 361 | return nil 362 | } 363 | */ 364 | -------------------------------------------------------------------------------- /meta/meta_test.go: -------------------------------------------------------------------------------- 1 | package meta_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math" 8 | "math/rand" 9 | "sort" 10 | "testing" 11 | "time" 12 | 13 | "collectd.org/meta" 14 | "github.com/google/go-cmp/cmp" 15 | "github.com/google/go-cmp/cmp/cmpopts" 16 | ) 17 | 18 | func ExampleData() { 19 | // Allocate new meta.Data object. 20 | m := meta.Data{ 21 | // Add interger named "answer": 22 | "answer": meta.Int64(42), 23 | // Add bool named "panic": 24 | "panic": meta.Bool(false), 25 | } 26 | 27 | // Add string named "required": 28 | m["required"] = meta.String("towel") 29 | 30 | // Remove the "panic" value: 31 | delete(m, "panic") 32 | } 33 | 34 | func ExampleData_exists() { 35 | m := meta.Data{ 36 | "answer": meta.Int64(42), 37 | "panic": meta.Bool(false), 38 | "required": meta.String("towel"), 39 | } 40 | 41 | for _, k := range []string{"answer", "question"} { 42 | _, ok := m[k] 43 | fmt.Println(k, "exists:", ok) 44 | } 45 | 46 | // Output: 47 | // answer exists: true 48 | // question exists: false 49 | } 50 | 51 | // This example demonstrates how to get a list of keys from meta.Data. 52 | func ExampleData_keys() { 53 | m := meta.Data{ 54 | "answer": meta.Int64(42), 55 | "panic": meta.Bool(false), 56 | "required": meta.String("towel"), 57 | } 58 | 59 | var keys []string 60 | for k := range m { 61 | keys = append(keys, k) 62 | } 63 | sort.Strings(keys) 64 | fmt.Println(keys) 65 | 66 | // Output: 67 | // [answer panic required] 68 | } 69 | 70 | func ExampleEntry() { 71 | // Allocate an int64 Entry. 72 | answer := meta.Int64(42) 73 | 74 | // Read back the "answer" value and ensure it is in fact an int64. 75 | a, ok := answer.Int64() 76 | if !ok { 77 | log.Fatal("Answer is not an int64") 78 | } 79 | fmt.Printf("The answer is between %d and %d\n", a-1, a+1) 80 | 81 | // Allocate a string Entry. 82 | required := meta.String("towel") 83 | 84 | // String is a bit different, because Entry.String() does not return a boolean. 85 | // Check that "required" is a string and read it into a variable. 86 | if !required.IsString() { 87 | log.Fatal("required is not a string") 88 | } 89 | fmt.Println("You need a " + required.String()) 90 | 91 | // The fmt.Stringer interface is implemented for all value types. To 92 | // print a string with default formatting, rely on the String() method: 93 | p := meta.Bool(false) 94 | fmt.Printf("Should I panic? %v\n", p) 95 | 96 | // Output: 97 | // The answer is between 41 and 43 98 | // You need a towel 99 | // Should I panic? false 100 | } 101 | 102 | func ExampleEntry_Interface() { 103 | rand.Seed(time.Now().UnixNano()) 104 | m := meta.Data{} 105 | 106 | // Create a value with unknown type. "key" is either a string, 107 | // or an int64. 108 | switch rand.Intn(2) { 109 | case 0: 110 | m["key"] = meta.String("value") 111 | case 1: 112 | m["key"] = meta.Int64(42) 113 | } 114 | 115 | // Scenario 0: A specific type is expected. Report an error that 116 | // includes the actual type in the error message, if the value is of a 117 | // different type. 118 | if _, ok := m["key"].Int64(); !ok { 119 | err := fmt.Errorf("key is a %T, want an int64", m["key"].Interface()) 120 | fmt.Println(err) // prints "key is a string, want an int64" 121 | } 122 | 123 | // Scenario 1: Multiple or all types need to be handled, for example to 124 | // encode the meta data values. The most elegant solution for that is a 125 | // type switch. 126 | switch v := m["key"].Interface().(type) { 127 | case string: 128 | // string-specific code here 129 | case int64: 130 | // The above code skipped printing this, so print it here so 131 | // this example produces the same output every time, despite 132 | // the randomness. 133 | fmt.Println("key is a string, want an int64") 134 | default: 135 | // Report the actual type if "key" is an unexpected type. 136 | err := fmt.Errorf("unexpected type %T", v) 137 | log.Fatal(err) 138 | } 139 | 140 | // Output: 141 | // key is a string, want an int64 142 | } 143 | 144 | func TestMarshalJSON(t *testing.T) { 145 | cases := []struct { 146 | d meta.Data 147 | want string 148 | }{ 149 | {meta.Data{"foo": meta.Bool(true)}, `{"foo":true}`}, 150 | {meta.Data{"foo": meta.Float64(20.0 / 3.0)}, `{"foo":6.666666666666667}`}, 151 | {meta.Data{"foo": meta.Float64(math.NaN())}, `{"foo":null}`}, 152 | {meta.Data{"foo": meta.Int64(-42)}, `{"foo":-42}`}, 153 | {meta.Data{"foo": meta.UInt64(42)}, `{"foo":42}`}, 154 | {meta.Data{"foo": meta.String(`Hello "World"!`)}, `{"foo":"Hello \"World\"!"}`}, 155 | {meta.Data{"foo": meta.Entry{}}, `{"foo":null}`}, 156 | } 157 | 158 | for _, tc := range cases { 159 | got, err := json.Marshal(tc.d) 160 | if err != nil { 161 | t.Errorf("json.Marshal(%#v) = %v", tc.d, err) 162 | continue 163 | } 164 | 165 | if diff := cmp.Diff(tc.want, string(got)); diff != "" { 166 | t.Errorf("json.Marshal(%#v) differs (+got/-want):\n%s", tc.d, diff) 167 | } 168 | } 169 | } 170 | 171 | func TestUnmarshalJSON(t *testing.T) { 172 | cases := []struct { 173 | in string 174 | want meta.Data 175 | wantErr bool 176 | }{ 177 | { 178 | in: `{}`, 179 | want: meta.Data{}, 180 | }, 181 | { 182 | in: `{"bool":true}`, 183 | want: meta.Data{"bool": meta.Bool(true)}, 184 | }, 185 | { 186 | in: `{"string":"bar"}`, 187 | want: meta.Data{"string": meta.String("bar")}, 188 | }, 189 | { 190 | in: `{"int":42}`, 191 | want: meta.Data{"int": meta.Int64(42)}, 192 | }, 193 | { // 9223372036854777144 exceeds 2^63-1 194 | in: `{"uint":9223372036854777144}`, 195 | want: meta.Data{"uint": meta.UInt64(9223372036854777144)}, 196 | }, 197 | { 198 | in: `{"float":42.25}`, 199 | want: meta.Data{"float": meta.Float64(42.25)}, 200 | }, 201 | { 202 | in: `{"float":null}`, 203 | want: meta.Data{"float": meta.Float64(math.NaN())}, 204 | }, 205 | { 206 | in: `{"bool":false,"string":"","int":-9223372036854775808,"uint":18446744073709551615,"float":0.00006103515625}`, 207 | want: meta.Data{ 208 | "bool": meta.Bool(false), 209 | "string": meta.String(""), 210 | "int": meta.Int64(-9223372036854775808), 211 | "uint": meta.UInt64(18446744073709551615), 212 | "float": meta.Float64(0.00006103515625), 213 | }, 214 | }, 215 | { 216 | in: `{"float":["invalid", "type"]}`, 217 | wantErr: true, 218 | }, 219 | } 220 | 221 | for _, c := range cases { 222 | var got meta.Data 223 | err := json.Unmarshal([]byte(c.in), &got) 224 | if gotErr := err != nil; gotErr != c.wantErr { 225 | t.Errorf("Unmarshal() = %v, want error: %v", err, c.wantErr) 226 | } 227 | if err != nil || c.wantErr { 228 | continue 229 | } 230 | 231 | opts := []cmp.Option{ 232 | cmp.AllowUnexported(meta.Entry{}), 233 | cmpopts.EquateNaNs(), 234 | } 235 | if diff := cmp.Diff(c.want, got, opts...); diff != "" { 236 | t.Errorf("Unmarshal() result differs (+got/-want):\n%s", diff) 237 | } 238 | } 239 | } 240 | 241 | func TestEntry(t *testing.T) { 242 | cases := []struct { 243 | typ string 244 | e meta.Entry 245 | wantBool bool 246 | wantFloat64 bool 247 | wantInt64 bool 248 | wantUInt64 bool 249 | wantString bool 250 | s string 251 | }{ 252 | { 253 | typ: "bool", 254 | e: meta.Bool(true), 255 | wantBool: true, 256 | s: "true", 257 | }, 258 | { 259 | typ: "float64", 260 | e: meta.Float64(20.0 / 3.0), 261 | wantFloat64: true, 262 | s: "6.66666666666667", 263 | }, 264 | { 265 | typ: "int64", 266 | e: meta.Int64(-9223372036854775808), 267 | wantInt64: true, 268 | s: "-9223372036854775808", 269 | }, 270 | { 271 | typ: "uint64", 272 | e: meta.UInt64(18446744073709551615), 273 | wantUInt64: true, 274 | s: "18446744073709551615", 275 | }, 276 | { 277 | typ: "string", 278 | e: meta.String("Hello, World!"), 279 | wantString: true, 280 | s: "Hello, World!", 281 | }, 282 | { 283 | // meta.Entry's zero value 284 | typ: "", 285 | s: "", 286 | }, 287 | } 288 | 289 | for _, tc := range cases { 290 | if v, got := tc.e.Bool(); got != tc.wantBool { 291 | t.Errorf("%#v.Bool() = (%v, %v), want (_, %v)", tc.e, v, got, tc.wantBool) 292 | } 293 | 294 | if v, got := tc.e.Float64(); got != tc.wantFloat64 { 295 | t.Errorf("%#v.Float64() = (%v, %v), want (_, %v)", tc.e, v, got, tc.wantFloat64) 296 | } 297 | 298 | if v, got := tc.e.Int64(); got != tc.wantInt64 { 299 | t.Errorf("%#v.Int64() = (%v, %v), want (_, %v)", tc.e, v, got, tc.wantInt64) 300 | } 301 | 302 | if v, got := tc.e.UInt64(); got != tc.wantUInt64 { 303 | t.Errorf("%#v.UInt64() = (%v, %v), want (_, %v)", tc.e, v, got, tc.wantUInt64) 304 | } 305 | 306 | if got := tc.e.IsString(); got != tc.wantString { 307 | t.Errorf("%#v.IsString() = %v, want %v", tc.e, got, tc.wantString) 308 | } 309 | 310 | if got, want := tc.e.String(), tc.s; got != want { 311 | t.Errorf("%#v.String() = %q, want %q", tc.e, got, want) 312 | } 313 | 314 | if got, want := fmt.Sprintf("%T", tc.e.Interface()), tc.typ; got != want { 315 | t.Errorf("%#v.Interface() = type %s, want type %s", tc.e, got, want) 316 | } 317 | } 318 | } 319 | 320 | func TestData_Clone(t *testing.T) { 321 | want := meta.Data{ 322 | "bool": meta.Bool(false), 323 | "string": meta.String(""), 324 | "int": meta.Int64(-9223372036854775808), 325 | "uint": meta.UInt64(18446744073709551615), 326 | "float": meta.Float64(0.00006103515625), 327 | } 328 | 329 | got := want.Clone() 330 | 331 | opts := []cmp.Option{ 332 | cmp.AllowUnexported(meta.Entry{}), 333 | cmpopts.EquateNaNs(), 334 | } 335 | if diff := cmp.Diff(want, got, opts...); diff != "" { 336 | t.Errorf("Data.Clone() contains differences (+got/-want):\n%s", diff) 337 | } 338 | 339 | want = nil 340 | if got := meta.Data(nil).Clone(); got != nil { 341 | t.Errorf("Data(nil).Clone() = %v, want %v", got, nil) 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /rpc/proto/collectd.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. 2 | // source: collectd.proto 3 | // DO NOT EDIT! 4 | 5 | /* 6 | Package proto is a generated protocol buffer package. 7 | 8 | It is generated from these files: 9 | collectd.proto 10 | 11 | It has these top-level messages: 12 | PutValuesRequest 13 | PutValuesResponse 14 | QueryValuesRequest 15 | QueryValuesResponse 16 | */ 17 | package proto 18 | 19 | import proto1 "github.com/golang/protobuf/proto" 20 | import fmt "fmt" 21 | import math "math" 22 | import collectd_types "collectd.org/rpc/proto/types" 23 | 24 | import ( 25 | context "golang.org/x/net/context" 26 | grpc "google.golang.org/grpc" 27 | ) 28 | 29 | // Reference imports to suppress errors if they are not otherwise used. 30 | var _ = proto1.Marshal 31 | var _ = fmt.Errorf 32 | var _ = math.Inf 33 | 34 | // This is a compile-time assertion to ensure that this generated file 35 | // is compatible with the proto package it is being compiled against. 36 | // A compilation error at this line likely means your copy of the 37 | // proto package needs to be updated. 38 | const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package 39 | 40 | // The arguments to PutValues. 41 | type PutValuesRequest struct { 42 | // value_list is the metric to be sent to the server. 43 | ValueList *collectd_types.ValueList `protobuf:"bytes,1,opt,name=value_list,json=valueList" json:"value_list,omitempty"` 44 | } 45 | 46 | func (m *PutValuesRequest) Reset() { *m = PutValuesRequest{} } 47 | func (m *PutValuesRequest) String() string { return proto1.CompactTextString(m) } 48 | func (*PutValuesRequest) ProtoMessage() {} 49 | func (*PutValuesRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 50 | 51 | func (m *PutValuesRequest) GetValueList() *collectd_types.ValueList { 52 | if m != nil { 53 | return m.ValueList 54 | } 55 | return nil 56 | } 57 | 58 | // The response from PutValues. 59 | type PutValuesResponse struct { 60 | } 61 | 62 | func (m *PutValuesResponse) Reset() { *m = PutValuesResponse{} } 63 | func (m *PutValuesResponse) String() string { return proto1.CompactTextString(m) } 64 | func (*PutValuesResponse) ProtoMessage() {} 65 | func (*PutValuesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 66 | 67 | // The arguments to QueryValues. 68 | type QueryValuesRequest struct { 69 | // Query by the fields of the identifier. Only return values matching the 70 | // specified shell wildcard patterns (see fnmatch(3)). Use '*' to match 71 | // any value. 72 | Identifier *collectd_types.Identifier `protobuf:"bytes,1,opt,name=identifier" json:"identifier,omitempty"` 73 | } 74 | 75 | func (m *QueryValuesRequest) Reset() { *m = QueryValuesRequest{} } 76 | func (m *QueryValuesRequest) String() string { return proto1.CompactTextString(m) } 77 | func (*QueryValuesRequest) ProtoMessage() {} 78 | func (*QueryValuesRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } 79 | 80 | func (m *QueryValuesRequest) GetIdentifier() *collectd_types.Identifier { 81 | if m != nil { 82 | return m.Identifier 83 | } 84 | return nil 85 | } 86 | 87 | // The response from QueryValues. 88 | type QueryValuesResponse struct { 89 | ValueList *collectd_types.ValueList `protobuf:"bytes,1,opt,name=value_list,json=valueList" json:"value_list,omitempty"` 90 | } 91 | 92 | func (m *QueryValuesResponse) Reset() { *m = QueryValuesResponse{} } 93 | func (m *QueryValuesResponse) String() string { return proto1.CompactTextString(m) } 94 | func (*QueryValuesResponse) ProtoMessage() {} 95 | func (*QueryValuesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } 96 | 97 | func (m *QueryValuesResponse) GetValueList() *collectd_types.ValueList { 98 | if m != nil { 99 | return m.ValueList 100 | } 101 | return nil 102 | } 103 | 104 | func init() { 105 | proto1.RegisterType((*PutValuesRequest)(nil), "collectd.PutValuesRequest") 106 | proto1.RegisterType((*PutValuesResponse)(nil), "collectd.PutValuesResponse") 107 | proto1.RegisterType((*QueryValuesRequest)(nil), "collectd.QueryValuesRequest") 108 | proto1.RegisterType((*QueryValuesResponse)(nil), "collectd.QueryValuesResponse") 109 | } 110 | 111 | // Reference imports to suppress errors if they are not otherwise used. 112 | var _ context.Context 113 | var _ grpc.ClientConn 114 | 115 | // This is a compile-time assertion to ensure that this generated file 116 | // is compatible with the grpc package it is being compiled against. 117 | const _ = grpc.SupportPackageIsVersion4 118 | 119 | // Client API for Collectd service 120 | 121 | type CollectdClient interface { 122 | // PutValues reads the value lists from the PutValuesRequest stream. 123 | // The gRPC server embedded into collectd will inject them into the system 124 | // just like the network plugin. 125 | PutValues(ctx context.Context, opts ...grpc.CallOption) (Collectd_PutValuesClient, error) 126 | // QueryValues returns a stream of matching value lists from collectd's 127 | // internal cache. 128 | QueryValues(ctx context.Context, in *QueryValuesRequest, opts ...grpc.CallOption) (Collectd_QueryValuesClient, error) 129 | } 130 | 131 | type collectdClient struct { 132 | cc *grpc.ClientConn 133 | } 134 | 135 | func NewCollectdClient(cc *grpc.ClientConn) CollectdClient { 136 | return &collectdClient{cc} 137 | } 138 | 139 | func (c *collectdClient) PutValues(ctx context.Context, opts ...grpc.CallOption) (Collectd_PutValuesClient, error) { 140 | stream, err := grpc.NewClientStream(ctx, &_Collectd_serviceDesc.Streams[0], c.cc, "/collectd.Collectd/PutValues", opts...) 141 | if err != nil { 142 | return nil, err 143 | } 144 | x := &collectdPutValuesClient{stream} 145 | return x, nil 146 | } 147 | 148 | type Collectd_PutValuesClient interface { 149 | Send(*PutValuesRequest) error 150 | CloseAndRecv() (*PutValuesResponse, error) 151 | grpc.ClientStream 152 | } 153 | 154 | type collectdPutValuesClient struct { 155 | grpc.ClientStream 156 | } 157 | 158 | func (x *collectdPutValuesClient) Send(m *PutValuesRequest) error { 159 | return x.ClientStream.SendMsg(m) 160 | } 161 | 162 | func (x *collectdPutValuesClient) CloseAndRecv() (*PutValuesResponse, error) { 163 | if err := x.ClientStream.CloseSend(); err != nil { 164 | return nil, err 165 | } 166 | m := new(PutValuesResponse) 167 | if err := x.ClientStream.RecvMsg(m); err != nil { 168 | return nil, err 169 | } 170 | return m, nil 171 | } 172 | 173 | func (c *collectdClient) QueryValues(ctx context.Context, in *QueryValuesRequest, opts ...grpc.CallOption) (Collectd_QueryValuesClient, error) { 174 | stream, err := grpc.NewClientStream(ctx, &_Collectd_serviceDesc.Streams[1], c.cc, "/collectd.Collectd/QueryValues", opts...) 175 | if err != nil { 176 | return nil, err 177 | } 178 | x := &collectdQueryValuesClient{stream} 179 | if err := x.ClientStream.SendMsg(in); err != nil { 180 | return nil, err 181 | } 182 | if err := x.ClientStream.CloseSend(); err != nil { 183 | return nil, err 184 | } 185 | return x, nil 186 | } 187 | 188 | type Collectd_QueryValuesClient interface { 189 | Recv() (*QueryValuesResponse, error) 190 | grpc.ClientStream 191 | } 192 | 193 | type collectdQueryValuesClient struct { 194 | grpc.ClientStream 195 | } 196 | 197 | func (x *collectdQueryValuesClient) Recv() (*QueryValuesResponse, error) { 198 | m := new(QueryValuesResponse) 199 | if err := x.ClientStream.RecvMsg(m); err != nil { 200 | return nil, err 201 | } 202 | return m, nil 203 | } 204 | 205 | // Server API for Collectd service 206 | 207 | type CollectdServer interface { 208 | // PutValues reads the value lists from the PutValuesRequest stream. 209 | // The gRPC server embedded into collectd will inject them into the system 210 | // just like the network plugin. 211 | PutValues(Collectd_PutValuesServer) error 212 | // QueryValues returns a stream of matching value lists from collectd's 213 | // internal cache. 214 | QueryValues(*QueryValuesRequest, Collectd_QueryValuesServer) error 215 | } 216 | 217 | func RegisterCollectdServer(s *grpc.Server, srv CollectdServer) { 218 | s.RegisterService(&_Collectd_serviceDesc, srv) 219 | } 220 | 221 | func _Collectd_PutValues_Handler(srv interface{}, stream grpc.ServerStream) error { 222 | return srv.(CollectdServer).PutValues(&collectdPutValuesServer{stream}) 223 | } 224 | 225 | type Collectd_PutValuesServer interface { 226 | SendAndClose(*PutValuesResponse) error 227 | Recv() (*PutValuesRequest, error) 228 | grpc.ServerStream 229 | } 230 | 231 | type collectdPutValuesServer struct { 232 | grpc.ServerStream 233 | } 234 | 235 | func (x *collectdPutValuesServer) SendAndClose(m *PutValuesResponse) error { 236 | return x.ServerStream.SendMsg(m) 237 | } 238 | 239 | func (x *collectdPutValuesServer) Recv() (*PutValuesRequest, error) { 240 | m := new(PutValuesRequest) 241 | if err := x.ServerStream.RecvMsg(m); err != nil { 242 | return nil, err 243 | } 244 | return m, nil 245 | } 246 | 247 | func _Collectd_QueryValues_Handler(srv interface{}, stream grpc.ServerStream) error { 248 | m := new(QueryValuesRequest) 249 | if err := stream.RecvMsg(m); err != nil { 250 | return err 251 | } 252 | return srv.(CollectdServer).QueryValues(m, &collectdQueryValuesServer{stream}) 253 | } 254 | 255 | type Collectd_QueryValuesServer interface { 256 | Send(*QueryValuesResponse) error 257 | grpc.ServerStream 258 | } 259 | 260 | type collectdQueryValuesServer struct { 261 | grpc.ServerStream 262 | } 263 | 264 | func (x *collectdQueryValuesServer) Send(m *QueryValuesResponse) error { 265 | return x.ServerStream.SendMsg(m) 266 | } 267 | 268 | var _Collectd_serviceDesc = grpc.ServiceDesc{ 269 | ServiceName: "collectd.Collectd", 270 | HandlerType: (*CollectdServer)(nil), 271 | Methods: []grpc.MethodDesc{}, 272 | Streams: []grpc.StreamDesc{ 273 | { 274 | StreamName: "PutValues", 275 | Handler: _Collectd_PutValues_Handler, 276 | ClientStreams: true, 277 | }, 278 | { 279 | StreamName: "QueryValues", 280 | Handler: _Collectd_QueryValues_Handler, 281 | ServerStreams: true, 282 | }, 283 | }, 284 | Metadata: "collectd.proto", 285 | } 286 | 287 | func init() { proto1.RegisterFile("collectd.proto", fileDescriptor0) } 288 | 289 | var fileDescriptor0 = []byte{ 290 | // 238 bytes of a gzipped FileDescriptorProto 291 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0xce, 0xcf, 0xc9, 292 | 0x49, 0x4d, 0x2e, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x80, 0xf1, 0xa5, 0xb8, 293 | 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0x21, 0xc2, 0x4a, 0x3e, 0x5c, 0x02, 0x01, 0xa5, 0x25, 0x61, 0x89, 294 | 0x39, 0xa5, 0xa9, 0xc5, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x16, 0x5c, 0x5c, 0x65, 295 | 0x20, 0x81, 0xf8, 0x9c, 0xcc, 0xe2, 0x12, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x6e, 0x23, 0x49, 0x3d, 296 | 0xb8, 0x79, 0x10, 0xed, 0x60, 0x2d, 0x3e, 0x99, 0xc5, 0x25, 0x41, 0x9c, 0x65, 0x30, 0xa6, 0x92, 297 | 0x30, 0x97, 0x20, 0x92, 0x69, 0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x4a, 0x01, 0x5c, 0x42, 0x81, 298 | 0xa5, 0xa9, 0x45, 0x95, 0xa8, 0x96, 0x58, 0x71, 0x71, 0x65, 0xa6, 0xa4, 0xe6, 0x95, 0x64, 0xa6, 299 | 0x65, 0xa6, 0x16, 0x41, 0x2d, 0x91, 0x42, 0xb7, 0xc4, 0x13, 0xae, 0x22, 0x08, 0x49, 0xb5, 0x92, 300 | 0x3f, 0x97, 0x30, 0x8a, 0x89, 0x10, 0x8b, 0xc8, 0x77, 0xb7, 0xd1, 0x02, 0x46, 0x2e, 0x0e, 0x67, 301 | 0xa8, 0x3a, 0x21, 0x37, 0x2e, 0x4e, 0xb8, 0x27, 0x84, 0x90, 0x9c, 0x84, 0x1e, 0x4e, 0x52, 0xd2, 302 | 0x58, 0xe5, 0x20, 0x8e, 0xd1, 0x60, 0x14, 0xf2, 0xe1, 0xe2, 0x46, 0x72, 0xa5, 0x90, 0x0c, 0x42, 303 | 0x35, 0x66, 0x70, 0x48, 0xc9, 0xe2, 0x90, 0x85, 0x98, 0x66, 0xc0, 0xe8, 0x24, 0x11, 0x25, 0x06, 304 | 0x57, 0x91, 0x5f, 0x94, 0xae, 0x5f, 0x54, 0x90, 0xac, 0x0f, 0x8e, 0xc2, 0x24, 0x36, 0x30, 0x65, 305 | 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xde, 0xc9, 0x28, 0x16, 0xf2, 0x01, 0x00, 0x00, 306 | } 307 | -------------------------------------------------------------------------------- /rpc/proto/types/types.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. 2 | // source: types.proto 3 | // DO NOT EDIT! 4 | 5 | /* 6 | Package types is a generated protocol buffer package. 7 | 8 | It is generated from these files: 9 | types.proto 10 | 11 | It has these top-level messages: 12 | Identifier 13 | Value 14 | ValueList 15 | */ 16 | package types 17 | 18 | import proto "github.com/golang/protobuf/proto" 19 | import fmt "fmt" 20 | import math "math" 21 | import google_protobuf "github.com/golang/protobuf/ptypes/duration" 22 | import google_protobuf1 "github.com/golang/protobuf/ptypes/timestamp" 23 | 24 | // Reference imports to suppress errors if they are not otherwise used. 25 | var _ = proto.Marshal 26 | var _ = fmt.Errorf 27 | var _ = math.Inf 28 | 29 | // This is a compile-time assertion to ensure that this generated file 30 | // is compatible with the proto package it is being compiled against. 31 | // A compilation error at this line likely means your copy of the 32 | // proto package needs to be updated. 33 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 34 | 35 | type Identifier struct { 36 | Host string `protobuf:"bytes,1,opt,name=host" json:"host,omitempty"` 37 | Plugin string `protobuf:"bytes,2,opt,name=plugin" json:"plugin,omitempty"` 38 | PluginInstance string `protobuf:"bytes,3,opt,name=plugin_instance,json=pluginInstance" json:"plugin_instance,omitempty"` 39 | Type string `protobuf:"bytes,4,opt,name=type" json:"type,omitempty"` 40 | TypeInstance string `protobuf:"bytes,5,opt,name=type_instance,json=typeInstance" json:"type_instance,omitempty"` 41 | } 42 | 43 | func (m *Identifier) Reset() { *m = Identifier{} } 44 | func (m *Identifier) String() string { return proto.CompactTextString(m) } 45 | func (*Identifier) ProtoMessage() {} 46 | func (*Identifier) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 47 | 48 | func (m *Identifier) GetHost() string { 49 | if m != nil { 50 | return m.Host 51 | } 52 | return "" 53 | } 54 | 55 | func (m *Identifier) GetPlugin() string { 56 | if m != nil { 57 | return m.Plugin 58 | } 59 | return "" 60 | } 61 | 62 | func (m *Identifier) GetPluginInstance() string { 63 | if m != nil { 64 | return m.PluginInstance 65 | } 66 | return "" 67 | } 68 | 69 | func (m *Identifier) GetType() string { 70 | if m != nil { 71 | return m.Type 72 | } 73 | return "" 74 | } 75 | 76 | func (m *Identifier) GetTypeInstance() string { 77 | if m != nil { 78 | return m.TypeInstance 79 | } 80 | return "" 81 | } 82 | 83 | type Value struct { 84 | // Types that are valid to be assigned to Value: 85 | // *Value_Counter 86 | // *Value_Gauge 87 | // *Value_Derive 88 | // *Value_Absolute 89 | Value isValue_Value `protobuf_oneof:"value"` 90 | } 91 | 92 | func (m *Value) Reset() { *m = Value{} } 93 | func (m *Value) String() string { return proto.CompactTextString(m) } 94 | func (*Value) ProtoMessage() {} 95 | func (*Value) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 96 | 97 | type isValue_Value interface { 98 | isValue_Value() 99 | } 100 | 101 | type Value_Counter struct { 102 | Counter uint64 `protobuf:"varint,1,opt,name=counter,oneof"` 103 | } 104 | type Value_Gauge struct { 105 | Gauge float64 `protobuf:"fixed64,2,opt,name=gauge,oneof"` 106 | } 107 | type Value_Derive struct { 108 | Derive int64 `protobuf:"varint,3,opt,name=derive,oneof"` 109 | } 110 | type Value_Absolute struct { 111 | Absolute uint64 `protobuf:"varint,4,opt,name=absolute,oneof"` 112 | } 113 | 114 | func (*Value_Counter) isValue_Value() {} 115 | func (*Value_Gauge) isValue_Value() {} 116 | func (*Value_Derive) isValue_Value() {} 117 | func (*Value_Absolute) isValue_Value() {} 118 | 119 | func (m *Value) GetValue() isValue_Value { 120 | if m != nil { 121 | return m.Value 122 | } 123 | return nil 124 | } 125 | 126 | func (m *Value) GetCounter() uint64 { 127 | if x, ok := m.GetValue().(*Value_Counter); ok { 128 | return x.Counter 129 | } 130 | return 0 131 | } 132 | 133 | func (m *Value) GetGauge() float64 { 134 | if x, ok := m.GetValue().(*Value_Gauge); ok { 135 | return x.Gauge 136 | } 137 | return 0 138 | } 139 | 140 | func (m *Value) GetDerive() int64 { 141 | if x, ok := m.GetValue().(*Value_Derive); ok { 142 | return x.Derive 143 | } 144 | return 0 145 | } 146 | 147 | func (m *Value) GetAbsolute() uint64 { 148 | if x, ok := m.GetValue().(*Value_Absolute); ok { 149 | return x.Absolute 150 | } 151 | return 0 152 | } 153 | 154 | // XXX_OneofFuncs is for the internal use of the proto package. 155 | func (*Value) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) { 156 | return _Value_OneofMarshaler, _Value_OneofUnmarshaler, _Value_OneofSizer, []interface{}{ 157 | (*Value_Counter)(nil), 158 | (*Value_Gauge)(nil), 159 | (*Value_Derive)(nil), 160 | (*Value_Absolute)(nil), 161 | } 162 | } 163 | 164 | func _Value_OneofMarshaler(msg proto.Message, b *proto.Buffer) error { 165 | m := msg.(*Value) 166 | // value 167 | switch x := m.Value.(type) { 168 | case *Value_Counter: 169 | b.EncodeVarint(1<<3 | proto.WireVarint) 170 | b.EncodeVarint(uint64(x.Counter)) 171 | case *Value_Gauge: 172 | b.EncodeVarint(2<<3 | proto.WireFixed64) 173 | b.EncodeFixed64(math.Float64bits(x.Gauge)) 174 | case *Value_Derive: 175 | b.EncodeVarint(3<<3 | proto.WireVarint) 176 | b.EncodeVarint(uint64(x.Derive)) 177 | case *Value_Absolute: 178 | b.EncodeVarint(4<<3 | proto.WireVarint) 179 | b.EncodeVarint(uint64(x.Absolute)) 180 | case nil: 181 | default: 182 | return fmt.Errorf("Value.Value has unexpected type %T", x) 183 | } 184 | return nil 185 | } 186 | 187 | func _Value_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) { 188 | m := msg.(*Value) 189 | switch tag { 190 | case 1: // value.counter 191 | if wire != proto.WireVarint { 192 | return true, proto.ErrInternalBadWireType 193 | } 194 | x, err := b.DecodeVarint() 195 | m.Value = &Value_Counter{x} 196 | return true, err 197 | case 2: // value.gauge 198 | if wire != proto.WireFixed64 { 199 | return true, proto.ErrInternalBadWireType 200 | } 201 | x, err := b.DecodeFixed64() 202 | m.Value = &Value_Gauge{math.Float64frombits(x)} 203 | return true, err 204 | case 3: // value.derive 205 | if wire != proto.WireVarint { 206 | return true, proto.ErrInternalBadWireType 207 | } 208 | x, err := b.DecodeVarint() 209 | m.Value = &Value_Derive{int64(x)} 210 | return true, err 211 | case 4: // value.absolute 212 | if wire != proto.WireVarint { 213 | return true, proto.ErrInternalBadWireType 214 | } 215 | x, err := b.DecodeVarint() 216 | m.Value = &Value_Absolute{x} 217 | return true, err 218 | default: 219 | return false, nil 220 | } 221 | } 222 | 223 | func _Value_OneofSizer(msg proto.Message) (n int) { 224 | m := msg.(*Value) 225 | // value 226 | switch x := m.Value.(type) { 227 | case *Value_Counter: 228 | n += proto.SizeVarint(1<<3 | proto.WireVarint) 229 | n += proto.SizeVarint(uint64(x.Counter)) 230 | case *Value_Gauge: 231 | n += proto.SizeVarint(2<<3 | proto.WireFixed64) 232 | n += 8 233 | case *Value_Derive: 234 | n += proto.SizeVarint(3<<3 | proto.WireVarint) 235 | n += proto.SizeVarint(uint64(x.Derive)) 236 | case *Value_Absolute: 237 | n += proto.SizeVarint(4<<3 | proto.WireVarint) 238 | n += proto.SizeVarint(uint64(x.Absolute)) 239 | case nil: 240 | default: 241 | panic(fmt.Sprintf("proto: unexpected type %T in oneof", x)) 242 | } 243 | return n 244 | } 245 | 246 | type ValueList struct { 247 | Values []*Value `protobuf:"bytes,1,rep,name=values" json:"values,omitempty"` 248 | Time *google_protobuf1.Timestamp `protobuf:"bytes,2,opt,name=time" json:"time,omitempty"` 249 | Interval *google_protobuf.Duration `protobuf:"bytes,3,opt,name=interval" json:"interval,omitempty"` 250 | Identifier *Identifier `protobuf:"bytes,4,opt,name=identifier" json:"identifier,omitempty"` 251 | DsNames []string `protobuf:"bytes,5,rep,name=ds_names,json=dsNames" json:"ds_names,omitempty"` 252 | } 253 | 254 | func (m *ValueList) Reset() { *m = ValueList{} } 255 | func (m *ValueList) String() string { return proto.CompactTextString(m) } 256 | func (*ValueList) ProtoMessage() {} 257 | func (*ValueList) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } 258 | 259 | func (m *ValueList) GetValues() []*Value { 260 | if m != nil { 261 | return m.Values 262 | } 263 | return nil 264 | } 265 | 266 | func (m *ValueList) GetTime() *google_protobuf1.Timestamp { 267 | if m != nil { 268 | return m.Time 269 | } 270 | return nil 271 | } 272 | 273 | func (m *ValueList) GetInterval() *google_protobuf.Duration { 274 | if m != nil { 275 | return m.Interval 276 | } 277 | return nil 278 | } 279 | 280 | func (m *ValueList) GetIdentifier() *Identifier { 281 | if m != nil { 282 | return m.Identifier 283 | } 284 | return nil 285 | } 286 | 287 | func (m *ValueList) GetDsNames() []string { 288 | if m != nil { 289 | return m.DsNames 290 | } 291 | return nil 292 | } 293 | 294 | func init() { 295 | proto.RegisterType((*Identifier)(nil), "collectd.types.Identifier") 296 | proto.RegisterType((*Value)(nil), "collectd.types.Value") 297 | proto.RegisterType((*ValueList)(nil), "collectd.types.ValueList") 298 | } 299 | 300 | func init() { proto.RegisterFile("types.proto", fileDescriptor0) } 301 | 302 | var fileDescriptor0 = []byte{ 303 | // 397 bytes of a gzipped FileDescriptorProto 304 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x52, 0x4d, 0xab, 0xd3, 0x40, 305 | 0x14, 0x6d, 0xcc, 0x47, 0xdb, 0x1b, 0x7d, 0xc2, 0x80, 0x8f, 0xbc, 0xf0, 0xa8, 0xa5, 0x2e, 0xec, 306 | 0xc6, 0x09, 0x54, 0xdc, 0xb8, 0x2c, 0x2e, 0x5a, 0x10, 0x17, 0x83, 0xb8, 0x70, 0x53, 0xa6, 0xc9, 307 | 0x34, 0x0e, 0xa4, 0x99, 0x90, 0x99, 0x14, 0x04, 0x7f, 0x89, 0xbf, 0xd4, 0xa5, 0xcc, 0x9d, 0x24, 308 | 0xd5, 0xbe, 0x55, 0xef, 0x3d, 0xf7, 0xdc, 0x73, 0xcf, 0xf4, 0x04, 0x62, 0xf3, 0xb3, 0x11, 0x9a, 309 | 0x36, 0xad, 0x32, 0x8a, 0xdc, 0xe5, 0xaa, 0xaa, 0x44, 0x6e, 0x0a, 0x8a, 0x68, 0xba, 0x28, 0x95, 310 | 0x2a, 0x2b, 0x91, 0xe1, 0xf4, 0xd8, 0x9d, 0xb2, 0xa2, 0x6b, 0xb9, 0x91, 0xaa, 0x76, 0xfc, 0xf4, 311 | 0xf5, 0xed, 0xdc, 0xc8, 0xb3, 0xd0, 0x86, 0x9f, 0x1b, 0x47, 0x58, 0xfd, 0xf6, 0x00, 0xf6, 0x85, 312 | 0xa8, 0x8d, 0x3c, 0x49, 0xd1, 0x12, 0x02, 0xc1, 0x0f, 0xa5, 0x4d, 0xe2, 0x2d, 0xbd, 0xf5, 0x9c, 313 | 0x61, 0x4d, 0xee, 0x21, 0x6a, 0xaa, 0xae, 0x94, 0x75, 0xf2, 0x0c, 0xd1, 0xbe, 0x23, 0x6f, 0xe1, 314 | 0xa5, 0xab, 0x0e, 0xb2, 0xd6, 0x86, 0xd7, 0xb9, 0x48, 0x7c, 0x24, 0xdc, 0x39, 0x78, 0xdf, 0xa3, 315 | 0x56, 0xd4, 0xba, 0x4d, 0x02, 0x27, 0x6a, 0x6b, 0xf2, 0x06, 0x5e, 0xd8, 0xdf, 0xeb, 0x6a, 0x88, 316 | 0xc3, 0xe7, 0x16, 0x1c, 0x16, 0x57, 0xbf, 0x20, 0xfc, 0xc6, 0xab, 0x4e, 0x90, 0x14, 0xa6, 0xb9, 317 | 0xea, 0x6a, 0x23, 0x5a, 0x74, 0x16, 0xec, 0x26, 0x6c, 0x00, 0xc8, 0x3d, 0x84, 0x25, 0xef, 0x4a, 318 | 0x81, 0xee, 0xbc, 0xdd, 0x84, 0xb9, 0x96, 0x24, 0x10, 0x15, 0xa2, 0x95, 0x17, 0xe7, 0xca, 0xdf, 319 | 0x4d, 0x58, 0xdf, 0x93, 0x47, 0x98, 0xf1, 0xa3, 0x56, 0x55, 0x67, 0x9c, 0x27, 0x2b, 0x37, 0x22, 320 | 0xdb, 0x29, 0x84, 0x17, 0x7b, 0x74, 0xf5, 0xc7, 0x83, 0x39, 0x9e, 0xff, 0x2c, 0xb5, 0x21, 0xef, 321 | 0x20, 0x42, 0x58, 0x27, 0xde, 0xd2, 0x5f, 0xc7, 0x9b, 0x57, 0xf4, 0xff, 0x28, 0x28, 0x52, 0x59, 322 | 0x4f, 0x22, 0x14, 0x02, 0xfb, 0x57, 0xa3, 0xa9, 0x78, 0x93, 0x52, 0x97, 0x03, 0x1d, 0x72, 0xa0, 323 | 0x5f, 0x87, 0x1c, 0x18, 0xf2, 0xc8, 0x07, 0x98, 0x49, 0xfb, 0x9c, 0x0b, 0xaf, 0xd0, 0x6f, 0xbc, 324 | 0x79, 0x78, 0xb2, 0xf3, 0xa9, 0xcf, 0x96, 0x8d, 0x54, 0xf2, 0x11, 0x40, 0x8e, 0xe9, 0xe1, 0x63, 325 | 0xec, 0xb1, 0x1b, 0x67, 0xd7, 0x7c, 0xd9, 0x3f, 0x6c, 0xf2, 0x00, 0xb3, 0x42, 0x1f, 0x6a, 0x7e, 326 | 0x16, 0x3a, 0x09, 0x97, 0xfe, 0x7a, 0xce, 0xa6, 0x85, 0xfe, 0x62, 0xdb, 0xed, 0xe2, 0xfb, 0xe3, 327 | 0xa8, 0xa1, 0xda, 0x32, 0x6b, 0x9b, 0xdc, 0x7d, 0x42, 0x19, 0x2a, 0x1e, 0x23, 0x6c, 0xde, 0xff, 328 | 0x0d, 0x00, 0x00, 0xff, 0xff, 0x0b, 0x72, 0x88, 0x50, 0x9c, 0x02, 0x00, 0x00, 329 | } 330 | --------------------------------------------------------------------------------