package collectd_test import ( "encoding/hex" "fmt" "net" "testing" "time" "github.com/influxdb/influxdb" "github.com/influxdb/influxdb/collectd" "github.com/kimor79/gollectd" ) type testServer string type serverResponses []serverResponse type serverResponse struct { database string retentionPolicy string points []influxdb.Point } var responses = make(chan *serverResponse, 1024) func (testServer) WriteSeries(database, retentionPolicy string, points []influxdb.Point) (uint64, error) { responses <- &serverResponse{ database: database, retentionPolicy: retentionPolicy, points: points, } return 0, nil } func (testServer) ResponseN(n int) ([]*serverResponse, error) { var a []*serverResponse for { select { case r := <-responses: a = append(a, r) if len(a) == n { return a, nil } case <-time.After(time.Second): return a, fmt.Errorf("unexpected response count: expected: %d, actual: %d", n, len(a)) } } } func TestServer_ListenAndServe_ErrBindAddressRequired(t *testing.T) { var ( ts testServer s = collectd.NewServer(ts, "foo") ) e := collectd.ListenAndServe(s, "") if e == nil { t.Fatalf("expected an error, got %v", e) } } func TestServer_ListenAndServe_ErrDatabaseNotSpecified(t *testing.T) { var ( ts testServer s = collectd.NewServer(ts, "foo") ) e := collectd.ListenAndServe(s, "127.0.0.1:25826") if e == nil { t.Fatalf("expected an error, got %v", e) } } func TestServer_ListenAndServe_ErrCouldNotParseTypesDBFile(t *testing.T) { var ( ts testServer s = collectd.NewServer(ts, "foo") ) s.Database = "foo" e := collectd.ListenAndServe(s, "127.0.0.1:25829") if e == nil { t.Fatalf("expected an error, got %v", e) } } func TestServer_ListenAndServe_Success(t *testing.T) { var ( ts testServer // You can typically find this on your mac here: "/usr/local/Cellar/collectd/5.4.1/share/collectd/types.db" s = collectd.NewServer(ts, "./collectd_test.conf") ) s.Database = "counter" e := collectd.ListenAndServe(s, "127.0.0.1:25830") defer s.Close() if e != nil { t.Fatalf("err does not match. expected %v, got %v", nil, e) } } func TestServer_Close_ErrServerClosed(t *testing.T) { var ( ts testServer // You can typically find this on your mac here: "/usr/local/Cellar/collectd/5.4.1/share/collectd/types.db" s = collectd.NewServer(ts, "./collectd_test.conf") ) s.Database = "counter" e := collectd.ListenAndServe(s, "127.0.0.1:25830") if e != nil { t.Fatalf("err does not match. expected %v, got %v", nil, e) } s.Close() e = s.Close() if e == nil { t.Fatalf("expected an error, got %v", e) } } func TestServer_ListenAndServe_ErrResolveUDPAddr(t *testing.T) { var ( ts testServer s = collectd.NewServer(ts, "./collectd_test.conf") ) s.Database = "counter" e := collectd.ListenAndServe(s, "foo") if e == nil { t.Fatalf("expected an error, got %v", e) } } func TestServer_ListenAndServe_ErrListenUDP(t *testing.T) { var ( ts testServer s = collectd.NewServer(ts, "./collectd_test.conf") ) //Open a udp listener on the port prior to force it to err addr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:25826") conn, _ := net.ListenUDP("udp", addr) defer conn.Close() s.Database = "counter" e := collectd.ListenAndServe(s, "127.0.0.1:25826") if e == nil { t.Fatalf("expected an error, got %v", e) } } func TestServer_Serve_Success(t *testing.T) { // clear any previous responses var ( ts testServer // You can typically find this on your mac here: "/usr/local/Cellar/collectd/5.4.1/share/collectd/types.db" s = collectd.NewServer(ts, "./collectd_test.conf") addr = "127.0.0.1:25830" ) s.Database = "counter" e := collectd.ListenAndServe(s, addr) defer s.Close() if e != nil { t.Fatalf("err does not match. expected %v, got %v", nil, e) } conn, e := net.Dial("udp", addr) defer conn.Close() if e != nil { t.Fatalf("err does not match. expected %v, got %v", nil, e) } buf, e := hex.DecodeString("0000000e6c6f63616c686f7374000008000c1512b2e40f5da16f0009000c00000002800000000002000e70726f636573736573000004000d70735f7374617465000005000c72756e6e696e67000006000f000101000000000000f03f0008000c1512b2e40f5db90f0005000d736c656570696e67000006000f0001010000000000c06f400008000c1512b2e40f5dc4a40005000c7a6f6d62696573000006000f00010100000000000000000008000c1512b2e40f5de10b0005000c73746f70706564000006000f00010100000000000000000008000c1512b2e40f5deac20005000b706167696e67000006000f00010100000000000000000008000c1512b2e40f5df59b0005000c626c6f636b6564000006000f00010100000000000000000008000c1512b2e40f7ee0610004000e666f726b5f726174650000050005000006000f000102000000000004572f0008000c1512b2e68e0635e6000200086370750000030006300000040008637075000005000975736572000006000f0001020000000000204f9c0008000c1512b2e68e0665d6000500096e696365000006000f000102000000000000caa30008000c1512b2e68e06789c0005000b73797374656d000006000f00010200000000000607050008000c1512b2e68e06818e0005000969646c65000006000f0001020000000003b090ae0008000c1512b2e68e068bcf0005000977616974000006000f000102000000000000f6810008000c1512b2e68e069c7d0005000e696e74657272757074000006000f000102000000000000001d0008000c1512b2e68e069fec0005000c736f6674697271000006000f0001020000000000000a2a0008000c1512b2e68e06a2b20005000a737465616c000006000f00010200000000000000000008000c1512b2e68e0708d60003000631000005000975736572000006000f00010200000000001d48c60008000c1512b2e68e070c16000500096e696365000006000f0001020000000000007fe60008000c1512b2e68e0710790005000b73797374656d000006000f00010200000000000667890008000c1512b2e68e0713bb0005000969646c65000006000f00010200000000025d0e470008000c1512b2e68e0717790005000977616974000006000f000102000000000002500e0008000c1512b2e68e071bc00005000e696e74657272757074000006000f00010200000000000000000008000c1512b2e68e071f800005000c736f6674697271000006000f00010200000000000006050008000c1512b2e68e07221e0005000a737465616c000006000f00010200000000000000000008000c1512b2e68e0726eb0003000632000005000975736572000006000f00010200000000001ff3e40008000c1512b2e68e0728cb000500096e696365000006000f000102000000000000ca210008000c1512b2e68e072ae70005000b73797374656d000006000f000102000000000006eabe0008000c1512b2e68e072f2f0005000977616974000006000f000102000000000000c1300008000c1512b2e68e072ccb0005000969646c65000006000f00010200000000025b5abb0008000c1512b2e68e07312c0005000e696e74657272757074000006000f00010200000000000000070008000c1512b2e68e0733520005000c736f6674697271000006000f00010200000000000007260008000c1512b2e68e0735b60005000a737465616c000006000f00010200000000000000000008000c1512b2e68e07828d0003000633000005000975736572000006000f000102000000000020f50a0008000c1512b2e68e0787ac000500096e696365000006000f0001020000000000008368") if e != nil { t.Fatalf("err from hex.DecodeString does not match. expected %v, got %v", nil, e) } _, e = conn.Write(buf) if e != nil { t.Fatalf("err does not match. expected %v, got %v", nil, e) } if _, err := ts.ResponseN(33); err != nil { t.Fatal(err) } } func TestUnmarshal_Points(t *testing.T) { /* This is a sample of what data can be represented like in json [ { "values": [197141504, 175136768], "dstypes": ["counter", "counter"], "dsnames": ["read", "write"], "time": 1251533299, "interval": 10, "host": "leeloo.lan.home.verplant.org", "plugin": "disk", "plugin_instance": "sda", "type": "disk_octets", "type_instance": "" }, … ] */ var tests = []struct { name string packet gollectd.Packet points []influxdb.Point }{ { name: "single value", points: []influxdb.Point{ {Name: "disk_read", Fields: map[string]interface{}{"disk_read": float64(1)}}, }, packet: gollectd.Packet{ Plugin: "disk", Values: []gollectd.Value{ {Name: "read", Value: 1}, }, }, }, { name: "multi value", points: []influxdb.Point{ {Name: "disk_read", Fields: map[string]interface{}{"disk_read": float64(1)}}, {Name: "disk_write", Fields: map[string]interface{}{"disk_write": float64(5)}}, }, packet: gollectd.Packet{ Plugin: "disk", Values: []gollectd.Value{ {Name: "read", Value: 1}, {Name: "write", Value: 5}, }, }, }, { name: "tags", points: []influxdb.Point{ { Name: "disk_read", Tags: map[string]string{"host": "server01", "instance": "sdk", "type": "disk_octets", "type_instance": "single"}, Fields: map[string]interface{}{"disk_read": float64(1)}, }, }, packet: gollectd.Packet{ Plugin: "disk", Hostname: "server01", PluginInstance: "sdk", Type: "disk_octets", TypeInstance: "single", Values: []gollectd.Value{ {Name: "read", Value: 1}, }, }, }, } for _, test := range tests { t.Logf("testing %q", test.name) points := collectd.Unmarshal(&test.packet) if len(points) != len(test.points) { t.Errorf("points len mismatch. expected %d, got %d", len(test.points), len(points)) } for i, m := range test.points { // test name name := fmt.Sprintf("%s_%s", test.packet.Plugin, test.packet.Values[i].Name) if m.Name != name { t.Errorf("point name mismatch. expected %q, got %q", name, m.Name) } // test value mv := m.Fields[m.Name].(float64) pv := test.packet.Values[i].Value if mv != pv { t.Errorf("point value mismatch. expected %v, got %v", pv, mv) } // test tags if test.packet.Hostname != m.Tags["host"] { t.Errorf(`point tags["host"] mismatch. expected %q, got %q`, test.packet.Hostname, m.Tags["host"]) } if test.packet.PluginInstance != m.Tags["instance"] { t.Errorf(`point tags["instance"] mismatch. expected %q, got %q`, test.packet.PluginInstance, m.Tags["instance"]) } if test.packet.Type != m.Tags["type"] { t.Errorf(`point tags["type"] mismatch. expected %q, got %q`, test.packet.Type, m.Tags["type"]) } if test.packet.TypeInstance != m.Tags["type_instance"] { t.Errorf(`point tags["type_instance"] mismatch. expected %q, got %q`, test.packet.TypeInstance, m.Tags["type_instance"]) } } } } func TestUnmarshal_Time(t *testing.T) { // Its important to remember that collectd stores high resolution time // as "near" nanoseconds (2^30) so we have to take that into account // when feeding time into the test. // Since we only store microseconds, we round it off (mostly to make testing easier) testTime := time.Now().UTC().Round(time.Microsecond) var timeHR = func(tm time.Time) uint64 { sec, nsec := tm.Unix(), tm.UnixNano()%1000000000 hr := (sec << 30) + (nsec * 1000000000 / 1073741824) return uint64(hr) } var tests = []struct { name string packet gollectd.Packet points []influxdb.Point }{ { name: "Should parse timeHR properly", packet: gollectd.Packet{ TimeHR: timeHR(testTime), Values: []gollectd.Value{ { Value: 1, }, }, }, points: []influxdb.Point{ {Timestamp: testTime}, }, }, { name: "Should parse time properly", packet: gollectd.Packet{ Time: uint64(testTime.Round(time.Second).Unix()), Values: []gollectd.Value{ { Value: 1, }, }, }, points: []influxdb.Point{ {Timestamp: testTime.Round(time.Second)}, }, }, } for _, test := range tests { t.Logf("testing %q", test.name) points := collectd.Unmarshal(&test.packet) if len(points) != len(test.points) { t.Errorf("point len mismatch. expected %d, got %d", len(test.points), len(points)) } for _, p := range points { if test.packet.TimeHR > 0 { if p.Timestamp.Format(time.RFC3339Nano) != testTime.Format(time.RFC3339Nano) { t.Errorf("timestamp mis-match, got %v, expected %v", p.Timestamp.Format(time.RFC3339Nano), testTime.Format(time.RFC3339Nano)) } else if p.Timestamp.Format(time.RFC3339) != testTime.Format(time.RFC3339) { t.Errorf("timestamp mis-match, got %v, expected %v", p.Timestamp.Format(time.RFC3339), testTime.Format(time.RFC3339)) } } } } }