renaming plugins -> inputs
This commit is contained in:
265
plugins/inputs/aerospike/README.md
Normal file
265
plugins/inputs/aerospike/README.md
Normal file
@@ -0,0 +1,265 @@
|
||||
## Telegraf Plugin: Aerospike
|
||||
|
||||
#### Plugin arguments:
|
||||
- **servers** string array: List of aerospike servers to query (def: 127.0.0.1:3000)
|
||||
|
||||
#### Description
|
||||
|
||||
The aerospike plugin queries aerospike server(s) and get node statistics. It also collects stats for
|
||||
all the configured namespaces.
|
||||
|
||||
For what the measurements mean, please consult the [Aerospike Metrics Reference Docs](http://www.aerospike.com/docs/reference/metrics).
|
||||
|
||||
The metric names, to make it less complicated in querying, have replaced all `-` with `_` as Aerospike metrics come in both forms (no idea why).
|
||||
|
||||
# Measurements:
|
||||
#### Aerospike Statistics [values]:
|
||||
|
||||
Meta:
|
||||
- units: Integer
|
||||
|
||||
Measurement names:
|
||||
- batch_index_queue
|
||||
- batch_index_unused_buffers
|
||||
- batch_queue
|
||||
- batch_tree_count
|
||||
- client_connections
|
||||
- data_used_bytes_memory
|
||||
- index_used_bytes_memory
|
||||
- info_queue
|
||||
- migrate_progress_recv
|
||||
- migrate_progress_send
|
||||
- migrate_rx_objs
|
||||
- migrate_tx_objs
|
||||
- objects
|
||||
- ongoing_write_reqs
|
||||
- partition_absent
|
||||
- partition_actual
|
||||
- partition_desync
|
||||
- partition_object_count
|
||||
- partition_ref_count
|
||||
- partition_replica
|
||||
- proxy_in_progress
|
||||
- query_agg_avg_rec_count
|
||||
- query_avg_rec_count
|
||||
- query_lookup_avg_rec_count
|
||||
- queue
|
||||
- record_locks
|
||||
- record_refs
|
||||
- sindex_used_bytes_memory
|
||||
- sindex_gc_garbage_cleaned
|
||||
- system_free_mem_pct
|
||||
- total_bytes_disk
|
||||
- total_bytes_memory
|
||||
- tree_count
|
||||
- scans_active
|
||||
- uptime
|
||||
- used_bytes_disk
|
||||
- used_bytes_memory
|
||||
- cluster_size
|
||||
- waiting_transactions
|
||||
|
||||
#### Aerospike Statistics [cumulative]:
|
||||
|
||||
Meta:
|
||||
- units: Integer
|
||||
|
||||
Measurement names:
|
||||
- batch_errors
|
||||
- batch_index_complete
|
||||
- batch_index_errors
|
||||
- batch_index_initiate
|
||||
- batch_index_timeout
|
||||
- batch_initiate
|
||||
- batch_timeout
|
||||
- err_duplicate_proxy_request
|
||||
- err_out_of_space
|
||||
- err_replica_non_null_node
|
||||
- err_replica_null_node
|
||||
- err_rw_cant_put_unique
|
||||
- err_rw_pending_limit
|
||||
- err_rw_request_not_found
|
||||
- err_storage_queue_full
|
||||
- err_sync_copy_null_master
|
||||
- err_sync_copy_null_node
|
||||
- err_tsvc_requests
|
||||
- err_write_fail_bin_exists
|
||||
- err_write_fail_generation
|
||||
- err_write_fail_generation_xdr
|
||||
- err_write_fail_incompatible_type
|
||||
- err_write_fail_key_exists
|
||||
- err_write_fail_key_mismatch
|
||||
- err_write_fail_not_found
|
||||
- err_write_fail_noxdr
|
||||
- err_write_fail_parameter
|
||||
- err_write_fail_prole_delete
|
||||
- err_write_fail_prole_generation
|
||||
- err_write_fail_prole_unknown
|
||||
- err_write_fail_unknown
|
||||
- fabric_msgs_rcvd
|
||||
- fabric_msgs_sent
|
||||
- heartbeat_received_foreign
|
||||
- heartbeat_received_self
|
||||
- migrate_msgs_recv
|
||||
- migrate_msgs_sent
|
||||
- migrate_num_incoming_accepted
|
||||
- migrate_num_incoming_refused
|
||||
- proxy_action
|
||||
- proxy_initiate
|
||||
- proxy_retry
|
||||
- proxy_retry_new_dest
|
||||
- proxy_retry_q_full
|
||||
- proxy_retry_same_dest
|
||||
- proxy_unproxy
|
||||
- query_abort
|
||||
- query_agg
|
||||
- query_agg_abort
|
||||
- query_agg_err
|
||||
- query_agg_success
|
||||
- query_bad_records
|
||||
- query_fail
|
||||
- query_long_queue_full
|
||||
- query_long_running
|
||||
- query_lookup_abort
|
||||
- query_lookup_err
|
||||
- query_lookups
|
||||
- query_lookup_success
|
||||
- query_reqs
|
||||
- query_short_queue_full
|
||||
- query_short_running
|
||||
- query_success
|
||||
- query_tracked
|
||||
- read_dup_prole
|
||||
- reaped_fds
|
||||
- rw_err_ack_badnode
|
||||
- rw_err_ack_internal
|
||||
- rw_err_ack_nomatch
|
||||
- rw_err_dup_cluster_key
|
||||
- rw_err_dup_internal
|
||||
- rw_err_dup_send
|
||||
- rw_err_write_cluster_key
|
||||
- rw_err_write_internal
|
||||
- rw_err_write_send
|
||||
- sindex_ucgarbage_found
|
||||
- sindex_gc_locktimedout
|
||||
- sindex_gc_inactivity_dur
|
||||
- sindex_gc_activity_dur
|
||||
- sindex_gc_list_creation_time
|
||||
- sindex_gc_list_deletion_time
|
||||
- sindex_gc_objects_validated
|
||||
- sindex_gc_garbage_found
|
||||
- stat_cluster_key_err_ack_dup_trans_reenqueue
|
||||
- stat_cluster_key_err_ack_rw_trans_reenqueue
|
||||
- stat_cluster_key_prole_retry
|
||||
- stat_cluster_key_regular_processed
|
||||
- stat_cluster_key_trans_to_proxy_retry
|
||||
- stat_deleted_set_object
|
||||
- stat_delete_success
|
||||
- stat_duplicate_operation
|
||||
- stat_evicted_objects
|
||||
- stat_evicted_objects_time
|
||||
- stat_evicted_set_objects
|
||||
- stat_expired_objects
|
||||
- stat_nsup_deletes_not_shipped
|
||||
- stat_proxy_errs
|
||||
- stat_proxy_reqs
|
||||
- stat_proxy_reqs_xdr
|
||||
- stat_proxy_success
|
||||
- stat_read_errs_notfound
|
||||
- stat_read_errs_other
|
||||
- stat_read_reqs
|
||||
- stat_read_reqs_xdr
|
||||
- stat_read_success
|
||||
- stat_rw_timeout
|
||||
- stat_slow_trans_queue_batch_pop
|
||||
- stat_slow_trans_queue_pop
|
||||
- stat_slow_trans_queue_push
|
||||
- stat_write_errs
|
||||
- stat_write_errs_notfound
|
||||
- stat_write_errs_other
|
||||
- stat_write_reqs
|
||||
- stat_write_reqs_xdr
|
||||
- stat_write_success
|
||||
- stat_xdr_pipe_miss
|
||||
- stat_xdr_pipe_writes
|
||||
- stat_zero_bin_records
|
||||
- storage_defrag_corrupt_record
|
||||
- storage_defrag_wait
|
||||
- transactions
|
||||
- basic_scans_succeeded
|
||||
- basic_scans_failed
|
||||
- aggr_scans_succeeded
|
||||
- aggr_scans_failed
|
||||
- udf_bg_scans_succeeded
|
||||
- udf_bg_scans_failed
|
||||
- udf_delete_err_others
|
||||
- udf_delete_reqs
|
||||
- udf_delete_success
|
||||
- udf_lua_errs
|
||||
- udf_query_rec_reqs
|
||||
- udf_read_errs_other
|
||||
- udf_read_reqs
|
||||
- udf_read_success
|
||||
- udf_replica_writes
|
||||
- udf_scan_rec_reqs
|
||||
- udf_write_err_others
|
||||
- udf_write_reqs
|
||||
- udf_write_success
|
||||
- write_master
|
||||
- write_prole
|
||||
|
||||
#### Aerospike Statistics [percentage]:
|
||||
|
||||
Meta:
|
||||
- units: percent (out of 100)
|
||||
|
||||
Measurement names:
|
||||
- free_pct_disk
|
||||
- free_pct_memory
|
||||
|
||||
# Measurements:
|
||||
#### Aerospike Namespace Statistics [values]:
|
||||
|
||||
Meta:
|
||||
- units: Integer
|
||||
- tags: `namespace=<namespace>`
|
||||
|
||||
Measurement names:
|
||||
- available_bin_names
|
||||
- available_pct
|
||||
- current_time
|
||||
- data_used_bytes_memory
|
||||
- index_used_bytes_memory
|
||||
- master_objects
|
||||
- max_evicted_ttl
|
||||
- max_void_time
|
||||
- non_expirable_objects
|
||||
- objects
|
||||
- prole_objects
|
||||
- sindex_used_bytes_memory
|
||||
- total_bytes_disk
|
||||
- total_bytes_memory
|
||||
- used_bytes_disk
|
||||
- used_bytes_memory
|
||||
|
||||
#### Aerospike Namespace Statistics [cumulative]:
|
||||
|
||||
Meta:
|
||||
- units: Integer
|
||||
- tags: `namespace=<namespace>`
|
||||
|
||||
Measurement names:
|
||||
- evicted_objects
|
||||
- expired_objects
|
||||
- set_deleted_objects
|
||||
- set_evicted_objects
|
||||
|
||||
#### Aerospike Namespace Statistics [percentage]:
|
||||
|
||||
Meta:
|
||||
- units: percent (out of 100)
|
||||
- tags: `namespace=<namespace>`
|
||||
|
||||
Measurement names:
|
||||
- free_pct_disk
|
||||
- free_pct_memory
|
||||
342
plugins/inputs/aerospike/aerospike.go
Normal file
342
plugins/inputs/aerospike/aerospike.go
Normal file
@@ -0,0 +1,342 @@
|
||||
package aerospike
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
MSG_HEADER_SIZE = 8
|
||||
MSG_TYPE = 1 // Info is 1
|
||||
MSG_VERSION = 2
|
||||
)
|
||||
|
||||
var (
|
||||
STATISTICS_COMMAND = []byte("statistics\n")
|
||||
NAMESPACES_COMMAND = []byte("namespaces\n")
|
||||
)
|
||||
|
||||
type aerospikeMessageHeader struct {
|
||||
Version uint8
|
||||
Type uint8
|
||||
DataLen [6]byte
|
||||
}
|
||||
|
||||
type aerospikeMessage struct {
|
||||
aerospikeMessageHeader
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// Taken from aerospike-client-go/types/message.go
|
||||
func (msg *aerospikeMessage) Serialize() []byte {
|
||||
msg.DataLen = msgLenToBytes(int64(len(msg.Data)))
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
binary.Write(buf, binary.BigEndian, msg.aerospikeMessageHeader)
|
||||
binary.Write(buf, binary.BigEndian, msg.Data[:])
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
type aerospikeInfoCommand struct {
|
||||
msg *aerospikeMessage
|
||||
}
|
||||
|
||||
// Taken from aerospike-client-go/info.go
|
||||
func (nfo *aerospikeInfoCommand) parseMultiResponse() (map[string]string, error) {
|
||||
responses := make(map[string]string)
|
||||
offset := int64(0)
|
||||
begin := int64(0)
|
||||
|
||||
dataLen := int64(len(nfo.msg.Data))
|
||||
|
||||
// Create reusable StringBuilder for performance.
|
||||
for offset < dataLen {
|
||||
b := nfo.msg.Data[offset]
|
||||
|
||||
if b == '\t' {
|
||||
name := nfo.msg.Data[begin:offset]
|
||||
offset++
|
||||
begin = offset
|
||||
|
||||
// Parse field value.
|
||||
for offset < dataLen {
|
||||
if nfo.msg.Data[offset] == '\n' {
|
||||
break
|
||||
}
|
||||
offset++
|
||||
}
|
||||
|
||||
if offset > begin {
|
||||
value := nfo.msg.Data[begin:offset]
|
||||
responses[string(name)] = string(value)
|
||||
} else {
|
||||
responses[string(name)] = ""
|
||||
}
|
||||
offset++
|
||||
begin = offset
|
||||
} else if b == '\n' {
|
||||
if offset > begin {
|
||||
name := nfo.msg.Data[begin:offset]
|
||||
responses[string(name)] = ""
|
||||
}
|
||||
offset++
|
||||
begin = offset
|
||||
} else {
|
||||
offset++
|
||||
}
|
||||
}
|
||||
|
||||
if offset > begin {
|
||||
name := nfo.msg.Data[begin:offset]
|
||||
responses[string(name)] = ""
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
type Aerospike struct {
|
||||
Servers []string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# Aerospike servers to connect to (with port)
|
||||
# Default: servers = ["localhost:3000"]
|
||||
#
|
||||
# This plugin will query all namespaces the aerospike
|
||||
# server has configured and get stats for them.
|
||||
servers = ["localhost:3000"]
|
||||
`
|
||||
|
||||
func (a *Aerospike) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (a *Aerospike) Description() string {
|
||||
return "Read stats from an aerospike server"
|
||||
}
|
||||
|
||||
func (a *Aerospike) Gather(acc inputs.Accumulator) error {
|
||||
if len(a.Servers) == 0 {
|
||||
return a.gatherServer("127.0.0.1:3000", acc)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var outerr error
|
||||
|
||||
for _, server := range a.Servers {
|
||||
wg.Add(1)
|
||||
go func(server string) {
|
||||
defer wg.Done()
|
||||
outerr = a.gatherServer(server, acc)
|
||||
}(server)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return outerr
|
||||
}
|
||||
|
||||
func (a *Aerospike) gatherServer(host string, acc inputs.Accumulator) error {
|
||||
aerospikeInfo, err := getMap(STATISTICS_COMMAND, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Aerospike info failed: %s", err)
|
||||
}
|
||||
readAerospikeStats(aerospikeInfo, acc, host, "")
|
||||
namespaces, err := getList(NAMESPACES_COMMAND, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Aerospike namespace list failed: %s", err)
|
||||
}
|
||||
for ix := range namespaces {
|
||||
nsInfo, err := getMap([]byte("namespace/"+namespaces[ix]+"\n"), host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Aerospike namespace '%s' query failed: %s", namespaces[ix], err)
|
||||
}
|
||||
readAerospikeStats(nsInfo, acc, host, namespaces[ix])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getMap(key []byte, host string) (map[string]string, error) {
|
||||
data, err := get(key, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get data: %s", err)
|
||||
}
|
||||
parsed, err := unmarshalMapInfo(data, string(key))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to unmarshal data: %s", err)
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func getList(key []byte, host string) ([]string, error) {
|
||||
data, err := get(key, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get data: %s", err)
|
||||
}
|
||||
parsed, err := unmarshalListInfo(data, string(key))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to unmarshal data: %s", err)
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func get(key []byte, host string) (map[string]string, error) {
|
||||
var err error
|
||||
var data map[string]string
|
||||
|
||||
asInfo := &aerospikeInfoCommand{
|
||||
msg: &aerospikeMessage{
|
||||
aerospikeMessageHeader: aerospikeMessageHeader{
|
||||
Version: uint8(MSG_VERSION),
|
||||
Type: uint8(MSG_TYPE),
|
||||
DataLen: msgLenToBytes(int64(len(key))),
|
||||
},
|
||||
Data: key,
|
||||
},
|
||||
}
|
||||
|
||||
cmd := asInfo.msg.Serialize()
|
||||
addr, err := net.ResolveTCPAddr("tcp", host)
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("Lookup failed for '%s': %s", host, err)
|
||||
}
|
||||
|
||||
conn, err := net.DialTCP("tcp", nil, addr)
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("Connection failed for '%s': %s", host, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write(cmd)
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("Failed to send to '%s': %s", host, err)
|
||||
}
|
||||
|
||||
msgHeader := bytes.NewBuffer(make([]byte, MSG_HEADER_SIZE))
|
||||
_, err = readLenFromConn(conn, msgHeader.Bytes(), MSG_HEADER_SIZE)
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("Failed to read header: %s", err)
|
||||
}
|
||||
err = binary.Read(msgHeader, binary.BigEndian, &asInfo.msg.aerospikeMessageHeader)
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("Failed to unmarshal header: %s", err)
|
||||
}
|
||||
|
||||
msgLen := msgLenFromBytes(asInfo.msg.aerospikeMessageHeader.DataLen)
|
||||
|
||||
if int64(len(asInfo.msg.Data)) != msgLen {
|
||||
asInfo.msg.Data = make([]byte, msgLen)
|
||||
}
|
||||
|
||||
_, err = readLenFromConn(conn, asInfo.msg.Data, len(asInfo.msg.Data))
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("Failed to read from connection to '%s': %s", host, err)
|
||||
}
|
||||
|
||||
data, err = asInfo.parseMultiResponse()
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("Failed to parse response from '%s': %s", host, err)
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
func readAerospikeStats(
|
||||
stats map[string]string,
|
||||
acc inputs.Accumulator,
|
||||
host string,
|
||||
namespace string,
|
||||
) {
|
||||
fields := make(map[string]interface{})
|
||||
tags := map[string]string{
|
||||
"aerospike_host": host,
|
||||
"namespace": "_service",
|
||||
}
|
||||
|
||||
if namespace != "" {
|
||||
tags["namespace"] = namespace
|
||||
}
|
||||
for key, value := range stats {
|
||||
// We are going to ignore all string based keys
|
||||
val, err := strconv.ParseInt(value, 10, 64)
|
||||
if err == nil {
|
||||
if strings.Contains(key, "-") {
|
||||
key = strings.Replace(key, "-", "_", -1)
|
||||
}
|
||||
fields[key] = val
|
||||
}
|
||||
}
|
||||
acc.AddFields("aerospike", fields, tags)
|
||||
}
|
||||
|
||||
func unmarshalMapInfo(infoMap map[string]string, key string) (map[string]string, error) {
|
||||
key = strings.TrimSuffix(key, "\n")
|
||||
res := map[string]string{}
|
||||
|
||||
v, exists := infoMap[key]
|
||||
if !exists {
|
||||
return res, fmt.Errorf("Key '%s' missing from info", key)
|
||||
}
|
||||
|
||||
values := strings.Split(v, ";")
|
||||
for i := range values {
|
||||
kv := strings.Split(values[i], "=")
|
||||
if len(kv) > 1 {
|
||||
res[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func unmarshalListInfo(infoMap map[string]string, key string) ([]string, error) {
|
||||
key = strings.TrimSuffix(key, "\n")
|
||||
|
||||
v, exists := infoMap[key]
|
||||
if !exists {
|
||||
return []string{}, fmt.Errorf("Key '%s' missing from info", key)
|
||||
}
|
||||
|
||||
values := strings.Split(v, ";")
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func readLenFromConn(c net.Conn, buffer []byte, length int) (total int, err error) {
|
||||
var r int
|
||||
for total < length {
|
||||
r, err = c.Read(buffer[total:length])
|
||||
total += r
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Taken from aerospike-client-go/types/message.go
|
||||
func msgLenToBytes(DataLen int64) [6]byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, uint64(DataLen))
|
||||
res := [6]byte{}
|
||||
copy(res[:], b[2:])
|
||||
return res
|
||||
}
|
||||
|
||||
// Taken from aerospike-client-go/types/message.go
|
||||
func msgLenFromBytes(buf [6]byte) int64 {
|
||||
nbytes := append([]byte{0, 0}, buf[:]...)
|
||||
DataLen := binary.BigEndian.Uint64(nbytes)
|
||||
return int64(DataLen)
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("aerospike", func() inputs.Input {
|
||||
return &Aerospike{}
|
||||
})
|
||||
}
|
||||
118
plugins/inputs/aerospike/aerospike_test.go
Normal file
118
plugins/inputs/aerospike/aerospike_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package aerospike
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAerospikeStatistics(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
a := &Aerospike{
|
||||
Servers: []string{testutil.GetLocalHost() + ":3000"},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := a.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Only use a few of the metrics
|
||||
asMetrics := []string{
|
||||
"transactions",
|
||||
"stat_write_errs",
|
||||
"stat_read_reqs",
|
||||
"stat_write_reqs",
|
||||
}
|
||||
|
||||
for _, metric := range asMetrics {
|
||||
assert.True(t, acc.HasIntField("aerospike", metric), metric)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAerospikeMsgLenFromToBytes(t *testing.T) {
|
||||
var i int64 = 8
|
||||
assert.True(t, i == msgLenFromBytes(msgLenToBytes(i)))
|
||||
}
|
||||
|
||||
func TestReadAerospikeStatsNoNamespace(t *testing.T) {
|
||||
// Also test for re-writing
|
||||
var acc testutil.Accumulator
|
||||
stats := map[string]string{
|
||||
"stat-write-errs": "12345",
|
||||
"stat_read_reqs": "12345",
|
||||
}
|
||||
readAerospikeStats(stats, &acc, "host1", "")
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"stat_write_errs": int64(12345),
|
||||
"stat_read_reqs": int64(12345),
|
||||
}
|
||||
tags := map[string]string{
|
||||
"aerospike_host": "host1",
|
||||
"namespace": "_service",
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "aerospike", fields, tags)
|
||||
}
|
||||
|
||||
func TestReadAerospikeStatsNamespace(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
stats := map[string]string{
|
||||
"stat_write_errs": "12345",
|
||||
"stat_read_reqs": "12345",
|
||||
}
|
||||
readAerospikeStats(stats, &acc, "host1", "test")
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"stat_write_errs": int64(12345),
|
||||
"stat_read_reqs": int64(12345),
|
||||
}
|
||||
tags := map[string]string{
|
||||
"aerospike_host": "host1",
|
||||
"namespace": "test",
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "aerospike", fields, tags)
|
||||
}
|
||||
|
||||
func TestAerospikeUnmarshalList(t *testing.T) {
|
||||
i := map[string]string{
|
||||
"test": "one;two;three",
|
||||
}
|
||||
|
||||
expected := []string{"one", "two", "three"}
|
||||
|
||||
list, err := unmarshalListInfo(i, "test2")
|
||||
assert.True(t, err != nil)
|
||||
|
||||
list, err = unmarshalListInfo(i, "test")
|
||||
assert.True(t, err == nil)
|
||||
equal := true
|
||||
for ix := range expected {
|
||||
if list[ix] != expected[ix] {
|
||||
equal = false
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, equal)
|
||||
}
|
||||
|
||||
func TestAerospikeUnmarshalMap(t *testing.T) {
|
||||
i := map[string]string{
|
||||
"test": "key1=value1;key2=value2",
|
||||
}
|
||||
|
||||
expected := map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
}
|
||||
m, err := unmarshalMapInfo(i, "test")
|
||||
assert.True(t, err == nil)
|
||||
assert.True(t, reflect.DeepEqual(m, expected))
|
||||
}
|
||||
37
plugins/inputs/all/all.go
Normal file
37
plugins/inputs/all/all.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package all
|
||||
|
||||
import (
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/aerospike"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/apache"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/bcache"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/disque"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/elasticsearch"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/exec"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/haproxy"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/httpjson"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/influxdb"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/jolokia"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/kafka_consumer"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/leofs"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/lustre2"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/mailchimp"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/memcached"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/mongodb"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/mysql"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/nginx"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/phpfpm"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/ping"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/postgresql"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/procstat"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/prometheus"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/puppetagent"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/rabbitmq"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/redis"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/rethinkdb"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/statsd"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/system"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/trig"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/twemproxy"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/zfs"
|
||||
_ "github.com/influxdb/telegraf/plugins/inputs/zookeeper"
|
||||
)
|
||||
45
plugins/inputs/apache/README.md
Normal file
45
plugins/inputs/apache/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Telegraf plugin: Apache
|
||||
|
||||
#### Plugin arguments:
|
||||
- **urls** []string: List of apache-status URLs to collect from.
|
||||
|
||||
#### Description
|
||||
|
||||
The Apache plugin collects from the /server-status?auto URL. See
|
||||
[apache.org/server-status?auto](http://www.apache.org/server-status?auto) for an
|
||||
example. And
|
||||
[here](http://httpd.apache.org/docs/2.2/mod/mod_status.html) for the apache
|
||||
mod_status documentation.
|
||||
|
||||
# Measurements:
|
||||
|
||||
Meta:
|
||||
- tags: `port=<port>`, `server=url`
|
||||
|
||||
- apache_TotalAccesses
|
||||
- apache_TotalkBytes
|
||||
- apache_CPULoad
|
||||
- apache_Uptime
|
||||
- apache_ReqPerSec
|
||||
- apache_BytesPerSec
|
||||
- apache_BytesPerReq
|
||||
- apache_BusyWorkers
|
||||
- apache_IdleWorkers
|
||||
- apache_ConnsTotal
|
||||
- apache_ConnsAsyncWriting
|
||||
- apache_ConnsAsyncKeepAlive
|
||||
- apache_ConnsAsyncClosing
|
||||
|
||||
### Scoreboard measurements
|
||||
|
||||
- apache_scboard_waiting
|
||||
- apache_scboard_starting
|
||||
- apache_scboard_reading
|
||||
- apache_scboard_sending
|
||||
- apache_scboard_keepalive
|
||||
- apache_scboard_dnslookup
|
||||
- apache_scboard_closing
|
||||
- apache_scboard_logging
|
||||
- apache_scboard_finishing
|
||||
- apache_scboard_idle_cleanup
|
||||
- apache_scboard_open
|
||||
170
plugins/inputs/apache/apache.go
Normal file
170
plugins/inputs/apache/apache.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package apache
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type Apache struct {
|
||||
Urls []string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of Apache status URI to gather stats.
|
||||
urls = ["http://localhost/server-status?auto"]
|
||||
`
|
||||
|
||||
func (n *Apache) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (n *Apache) Description() string {
|
||||
return "Read Apache status information (mod_status)"
|
||||
}
|
||||
|
||||
func (n *Apache) Gather(acc inputs.Accumulator) error {
|
||||
var wg sync.WaitGroup
|
||||
var outerr error
|
||||
|
||||
for _, u := range n.Urls {
|
||||
addr, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse address '%s': %s", u, err)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(addr *url.URL) {
|
||||
defer wg.Done()
|
||||
outerr = n.gatherUrl(addr, acc)
|
||||
}(addr)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
var tr = &http.Transport{
|
||||
ResponseHeaderTimeout: time.Duration(3 * time.Second),
|
||||
}
|
||||
|
||||
var client = &http.Client{Transport: tr}
|
||||
|
||||
func (n *Apache) gatherUrl(addr *url.URL, acc inputs.Accumulator) error {
|
||||
resp, err := client.Get(addr.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error making HTTP request to %s: %s", addr.String(), err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%s returned HTTP status %s", addr.String(), resp.Status)
|
||||
}
|
||||
|
||||
tags := getTags(addr)
|
||||
|
||||
sc := bufio.NewScanner(resp.Body)
|
||||
fields := make(map[string]interface{})
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
if strings.Contains(line, ":") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
key, part := strings.Replace(parts[0], " ", "", -1), strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "Scoreboard":
|
||||
for field, value := range n.gatherScores(part) {
|
||||
fields[field] = value
|
||||
}
|
||||
default:
|
||||
value, err := strconv.ParseFloat(part, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fields[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
acc.AddFields("apache", fields, tags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Apache) gatherScores(data string) map[string]interface{} {
|
||||
var waiting, open int = 0, 0
|
||||
var S, R, W, K, D, C, L, G, I int = 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
|
||||
for _, s := range strings.Split(data, "") {
|
||||
|
||||
switch s {
|
||||
case "_":
|
||||
waiting++
|
||||
case "S":
|
||||
S++
|
||||
case "R":
|
||||
R++
|
||||
case "W":
|
||||
W++
|
||||
case "K":
|
||||
K++
|
||||
case "D":
|
||||
D++
|
||||
case "C":
|
||||
C++
|
||||
case "L":
|
||||
L++
|
||||
case "G":
|
||||
G++
|
||||
case "I":
|
||||
I++
|
||||
case ".":
|
||||
open++
|
||||
}
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"scboard_waiting": float64(waiting),
|
||||
"scboard_starting": float64(S),
|
||||
"scboard_reading": float64(R),
|
||||
"scboard_sending": float64(W),
|
||||
"scboard_keepalive": float64(K),
|
||||
"scboard_dnslookup": float64(D),
|
||||
"scboard_closing": float64(C),
|
||||
"scboard_logging": float64(L),
|
||||
"scboard_finishing": float64(G),
|
||||
"scboard_idle_cleanup": float64(I),
|
||||
"scboard_open": float64(open),
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// Get tag(s) for the apache plugin
|
||||
func getTags(addr *url.URL) map[string]string {
|
||||
h := addr.Host
|
||||
host, port, err := net.SplitHostPort(h)
|
||||
if err != nil {
|
||||
host = addr.Host
|
||||
if addr.Scheme == "http" {
|
||||
port = "80"
|
||||
} else if addr.Scheme == "https" {
|
||||
port = "443"
|
||||
} else {
|
||||
port = ""
|
||||
}
|
||||
}
|
||||
return map[string]string{"server": host, "port": port}
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("apache", func() inputs.Input {
|
||||
return &Apache{}
|
||||
})
|
||||
}
|
||||
73
plugins/inputs/apache/apache_test.go
Normal file
73
plugins/inputs/apache/apache_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package apache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var apacheStatus = `
|
||||
Total Accesses: 129811861
|
||||
Total kBytes: 5213701865
|
||||
CPULoad: 6.51929
|
||||
Uptime: 941553
|
||||
ReqPerSec: 137.87
|
||||
BytesPerSec: 5670240
|
||||
BytesPerReq: 41127.4
|
||||
BusyWorkers: 270
|
||||
IdleWorkers: 630
|
||||
ConnsTotal: 1451
|
||||
ConnsAsyncWriting: 32
|
||||
ConnsAsyncKeepAlive: 945
|
||||
ConnsAsyncClosing: 205
|
||||
Scoreboard: WW_____W_RW_R_W__RRR____WR_W___WW________W_WW_W_____R__R_WR__WRWR_RRRW___R_RWW__WWWRW__R_RW___RR_RW_R__W__WR_WWW______WWR__R___R_WR_W___RW______RR________________W______R__RR______W________________R____R__________________________RW_W____R_____W_R_________________R____RR__W___R_R____RW______R____W______W_W_R_R______R__R_R__________R____W_______WW____W____RR__W_____W_R_______W__________W___W____________W_______WRR_R_W____W_____R____W_WW_R____RRW__W............................................................................................................................................................................................................................................................................................................WRRWR____WR__RR_R___RWR_________W_R____RWRRR____R_R__RW_R___WWW_RW__WR_RRR____W___R____WW_R__R___RR_W_W_RRRRWR__RRWR__RRW_W_RRRW_R_RR_W__RR_RWRR_R__R___RR_RR______R__RR____R_____W_R_R_R__R__R__________W____WW_R___R_R___R_________RR__RR____RWWWW___W_R________R_R____R_W___W___R___W_WRRWW_______R__W_RW_______R________RR__R________W_______________________W_W______________RW_________WR__R___R__R_______________WR_R_________W___RW_____R____________W____......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
|
||||
`
|
||||
|
||||
func TestHTTPApache(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, apacheStatus)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
a := Apache{
|
||||
Urls: []string{ts.URL},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := a.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"TotalAccesses": float64(1.29811861e+08),
|
||||
"TotalkBytes": float64(5.213701865e+09),
|
||||
"CPULoad": float64(6.51929),
|
||||
"Uptime": float64(941553),
|
||||
"ReqPerSec": float64(137.87),
|
||||
"BytesPerSec": float64(5.67024e+06),
|
||||
"BytesPerReq": float64(41127.4),
|
||||
"BusyWorkers": float64(270),
|
||||
"IdleWorkers": float64(630),
|
||||
"ConnsTotal": float64(1451),
|
||||
"ConnsAsyncWriting": float64(32),
|
||||
"ConnsAsyncKeepAlive": float64(945),
|
||||
"ConnsAsyncClosing": float64(205),
|
||||
"scboard_waiting": float64(630),
|
||||
"scboard_starting": float64(0),
|
||||
"scboard_reading": float64(157),
|
||||
"scboard_sending": float64(113),
|
||||
"scboard_keepalive": float64(0),
|
||||
"scboard_dnslookup": float64(0),
|
||||
"scboard_closing": float64(0),
|
||||
"scboard_logging": float64(0),
|
||||
"scboard_finishing": float64(0),
|
||||
"scboard_idle_cleanup": float64(0),
|
||||
"scboard_open": float64(2850),
|
||||
}
|
||||
acc.AssertContainsFields(t, "apache", fields)
|
||||
}
|
||||
89
plugins/inputs/bcache/README.md
Normal file
89
plugins/inputs/bcache/README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Telegraf plugin: bcache
|
||||
|
||||
Get bcache stat from stats_total directory and dirty_data file.
|
||||
|
||||
# Measurements
|
||||
|
||||
Meta:
|
||||
|
||||
- tags: `backing_dev=dev bcache_dev=dev`
|
||||
|
||||
Measurement names:
|
||||
|
||||
- dirty_data
|
||||
- bypassed
|
||||
- cache_bypass_hits
|
||||
- cache_bypass_misses
|
||||
- cache_hit_ratio
|
||||
- cache_hits
|
||||
- cache_miss_collisions
|
||||
- cache_misses
|
||||
- cache_readaheads
|
||||
|
||||
### Description
|
||||
|
||||
```
|
||||
dirty_data
|
||||
Amount of dirty data for this backing device in the cache. Continuously
|
||||
updated unlike the cache set's version, but may be slightly off.
|
||||
|
||||
bypassed
|
||||
Amount of IO (both reads and writes) that has bypassed the cache
|
||||
|
||||
|
||||
cache_bypass_hits
|
||||
cache_bypass_misses
|
||||
Hits and misses for IO that is intended to skip the cache are still counted,
|
||||
but broken out here.
|
||||
|
||||
cache_hits
|
||||
cache_misses
|
||||
cache_hit_ratio
|
||||
Hits and misses are counted per individual IO as bcache sees them; a
|
||||
partial hit is counted as a miss.
|
||||
|
||||
cache_miss_collisions
|
||||
Counts instances where data was going to be inserted into the cache from a
|
||||
cache miss, but raced with a write and data was already present (usually 0
|
||||
since the synchronization for cache misses was rewritten)
|
||||
|
||||
cache_readaheads
|
||||
Count of times readahead occurred.
|
||||
```
|
||||
|
||||
# Example output
|
||||
|
||||
Using this configuration:
|
||||
|
||||
```
|
||||
[bcache]
|
||||
# Bcache sets path
|
||||
# If not specified, then default is:
|
||||
# bcachePath = "/sys/fs/bcache"
|
||||
#
|
||||
# By default, telegraf gather stats for all bcache devices
|
||||
# Setting devices will restrict the stats to the specified
|
||||
# bcache devices.
|
||||
# bcacheDevs = ["bcache0", ...]
|
||||
```
|
||||
|
||||
When run with:
|
||||
|
||||
```
|
||||
./telegraf -config telegraf.conf -input-filter bcache -test
|
||||
```
|
||||
|
||||
It produces:
|
||||
|
||||
```
|
||||
* Plugin: bcache, Collection 1
|
||||
> [backing_dev="md10" bcache_dev="bcache0"] bcache_dirty_data value=11639194
|
||||
> [backing_dev="md10" bcache_dev="bcache0"] bcache_bypassed value=5167704440832
|
||||
> [backing_dev="md10" bcache_dev="bcache0"] bcache_cache_bypass_hits value=146270986
|
||||
> [backing_dev="md10" bcache_dev="bcache0"] bcache_cache_bypass_misses value=0
|
||||
> [backing_dev="md10" bcache_dev="bcache0"] bcache_cache_hit_ratio value=90
|
||||
> [backing_dev="md10" bcache_dev="bcache0"] bcache_cache_hits value=511941651
|
||||
> [backing_dev="md10" bcache_dev="bcache0"] bcache_cache_miss_collisions value=157678
|
||||
> [backing_dev="md10" bcache_dev="bcache0"] bcache_cache_misses value=50647396
|
||||
> [backing_dev="md10" bcache_dev="bcache0"] bcache_cache_readaheads value=0
|
||||
```
|
||||
141
plugins/inputs/bcache/bcache.go
Normal file
141
plugins/inputs/bcache/bcache.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package bcache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type Bcache struct {
|
||||
BcachePath string
|
||||
BcacheDevs []string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# Bcache sets path
|
||||
# If not specified, then default is:
|
||||
# bcachePath = "/sys/fs/bcache"
|
||||
#
|
||||
# By default, telegraf gather stats for all bcache devices
|
||||
# Setting devices will restrict the stats to the specified
|
||||
# bcache devices.
|
||||
# bcacheDevs = ["bcache0", ...]
|
||||
`
|
||||
|
||||
func (b *Bcache) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (b *Bcache) Description() string {
|
||||
return "Read metrics of bcache from stats_total and dirty_data"
|
||||
}
|
||||
|
||||
func getTags(bdev string) map[string]string {
|
||||
backingDevFile, _ := os.Readlink(bdev)
|
||||
backingDevPath := strings.Split(backingDevFile, "/")
|
||||
backingDev := backingDevPath[len(backingDevPath)-2]
|
||||
|
||||
bcacheDevFile, _ := os.Readlink(bdev + "/dev")
|
||||
bcacheDevPath := strings.Split(bcacheDevFile, "/")
|
||||
bcacheDev := bcacheDevPath[len(bcacheDevPath)-1]
|
||||
|
||||
return map[string]string{"backing_dev": backingDev, "bcache_dev": bcacheDev}
|
||||
}
|
||||
|
||||
func prettyToBytes(v string) uint64 {
|
||||
var factors = map[string]uint64{
|
||||
"k": 1 << 10,
|
||||
"M": 1 << 20,
|
||||
"G": 1 << 30,
|
||||
"T": 1 << 40,
|
||||
"P": 1 << 50,
|
||||
"E": 1 << 60,
|
||||
}
|
||||
var factor uint64
|
||||
factor = 1
|
||||
prefix := v[len(v)-1 : len(v)]
|
||||
if factors[prefix] != 0 {
|
||||
v = v[:len(v)-1]
|
||||
factor = factors[prefix]
|
||||
}
|
||||
result, _ := strconv.ParseFloat(v, 32)
|
||||
result = result * float64(factor)
|
||||
|
||||
return uint64(result)
|
||||
}
|
||||
|
||||
func (b *Bcache) gatherBcache(bdev string, acc inputs.Accumulator) error {
|
||||
tags := getTags(bdev)
|
||||
metrics, err := filepath.Glob(bdev + "/stats_total/*")
|
||||
if len(metrics) < 0 {
|
||||
return errors.New("Can't read any stats file")
|
||||
}
|
||||
file, err := ioutil.ReadFile(bdev + "/dirty_data")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawValue := strings.TrimSpace(string(file))
|
||||
value := prettyToBytes(rawValue)
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
fields["dirty_data"] = value
|
||||
|
||||
for _, path := range metrics {
|
||||
key := filepath.Base(path)
|
||||
file, err := ioutil.ReadFile(path)
|
||||
rawValue := strings.TrimSpace(string(file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key == "bypassed" {
|
||||
value := prettyToBytes(rawValue)
|
||||
fields[key] = value
|
||||
} else {
|
||||
value, _ := strconv.ParseUint(rawValue, 10, 64)
|
||||
fields[key] = value
|
||||
}
|
||||
}
|
||||
acc.AddFields("bcache", fields, tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bcache) Gather(acc inputs.Accumulator) error {
|
||||
bcacheDevsChecked := make(map[string]bool)
|
||||
var restrictDevs bool
|
||||
if len(b.BcacheDevs) != 0 {
|
||||
restrictDevs = true
|
||||
for _, bcacheDev := range b.BcacheDevs {
|
||||
bcacheDevsChecked[bcacheDev] = true
|
||||
}
|
||||
}
|
||||
|
||||
bcachePath := b.BcachePath
|
||||
if len(bcachePath) == 0 {
|
||||
bcachePath = "/sys/fs/bcache"
|
||||
}
|
||||
bdevs, _ := filepath.Glob(bcachePath + "/*/bdev*")
|
||||
if len(bdevs) < 1 {
|
||||
return errors.New("Can't find any bcache device")
|
||||
}
|
||||
for _, bdev := range bdevs {
|
||||
if restrictDevs {
|
||||
bcacheDev := getTags(bdev)["bcache_dev"]
|
||||
if !bcacheDevsChecked[bcacheDev] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
b.gatherBcache(bdev, acc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("bcache", func() inputs.Input {
|
||||
return &Bcache{}
|
||||
})
|
||||
}
|
||||
121
plugins/inputs/bcache/bcache_test.go
Normal file
121
plugins/inputs/bcache/bcache_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package bcache
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
dirty_data = "1.5G"
|
||||
bypassed = "4.7T"
|
||||
cache_bypass_hits = "146155333"
|
||||
cache_bypass_misses = "0"
|
||||
cache_hit_ratio = "90"
|
||||
cache_hits = "511469583"
|
||||
cache_miss_collisions = "157567"
|
||||
cache_misses = "50616331"
|
||||
cache_readaheads = "2"
|
||||
)
|
||||
|
||||
var (
|
||||
testBcachePath = os.TempDir() + "/telegraf/sys/fs/bcache"
|
||||
testBcacheUuidPath = testBcachePath + "/663955a3-765a-4737-a9fd-8250a7a78411"
|
||||
testBcacheDevPath = os.TempDir() + "/telegraf/sys/devices/virtual/block/bcache0"
|
||||
testBcacheBackingDevPath = os.TempDir() + "/telegraf/sys/devices/virtual/block/md10"
|
||||
)
|
||||
|
||||
func TestBcacheGeneratesMetrics(t *testing.T) {
|
||||
err := os.MkdirAll(testBcacheUuidPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(testBcacheDevPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(testBcacheBackingDevPath+"/bcache", 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.Symlink(testBcacheBackingDevPath+"/bcache", testBcacheUuidPath+"/bdev0")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.Symlink(testBcacheDevPath, testBcacheUuidPath+"/bdev0/dev")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(testBcacheUuidPath+"/bdev0/stats_total", 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(testBcacheUuidPath+"/bdev0/dirty_data",
|
||||
[]byte(dirty_data), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(testBcacheUuidPath+"/bdev0/stats_total/bypassed",
|
||||
[]byte(bypassed), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(testBcacheUuidPath+"/bdev0/stats_total/cache_bypass_hits",
|
||||
[]byte(cache_bypass_hits), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(testBcacheUuidPath+"/bdev0/stats_total/cache_bypass_misses",
|
||||
[]byte(cache_bypass_misses), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(testBcacheUuidPath+"/bdev0/stats_total/cache_hit_ratio",
|
||||
[]byte(cache_hit_ratio), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(testBcacheUuidPath+"/bdev0/stats_total/cache_hits",
|
||||
[]byte(cache_hits), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(testBcacheUuidPath+"/bdev0/stats_total/cache_miss_collisions",
|
||||
[]byte(cache_miss_collisions), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(testBcacheUuidPath+"/bdev0/stats_total/cache_misses",
|
||||
[]byte(cache_misses), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(testBcacheUuidPath+"/bdev0/stats_total/cache_readaheads",
|
||||
[]byte(cache_readaheads), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"dirty_data": uint64(1610612736),
|
||||
"bypassed": uint64(5167704440832),
|
||||
"cache_bypass_hits": uint64(146155333),
|
||||
"cache_bypass_misses": uint64(0),
|
||||
"cache_hit_ratio": uint64(90),
|
||||
"cache_hits": uint64(511469583),
|
||||
"cache_miss_collisions": uint64(157567),
|
||||
"cache_misses": uint64(50616331),
|
||||
"cache_readaheads": uint64(2),
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"backing_dev": "md10",
|
||||
"bcache_dev": "bcache0",
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
// all devs
|
||||
b := &Bcache{BcachePath: testBcachePath}
|
||||
|
||||
err = b.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
acc.AssertContainsTaggedFields(t, "bcache", fields, tags)
|
||||
|
||||
// one exist dev
|
||||
b = &Bcache{BcachePath: testBcachePath, BcacheDevs: []string{"bcache0"}}
|
||||
|
||||
err = b.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
acc.AssertContainsTaggedFields(t, "bcache", fields, tags)
|
||||
|
||||
err = os.RemoveAll(os.TempDir() + "/telegraf")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
204
plugins/inputs/disque/disque.go
Normal file
204
plugins/inputs/disque/disque.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package disque
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type Disque struct {
|
||||
Servers []string
|
||||
|
||||
c net.Conn
|
||||
buf []byte
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of URI to gather stats about. Specify an ip or hostname
|
||||
# with optional port and password. ie disque://localhost, disque://10.10.3.33:18832,
|
||||
# 10.0.0.1:10000, etc.
|
||||
#
|
||||
# If no servers are specified, then localhost is used as the host.
|
||||
servers = ["localhost"]
|
||||
`
|
||||
|
||||
func (r *Disque) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (r *Disque) Description() string {
|
||||
return "Read metrics from one or many disque servers"
|
||||
}
|
||||
|
||||
var Tracking = map[string]string{
|
||||
"uptime_in_seconds": "uptime",
|
||||
"connected_clients": "clients",
|
||||
"blocked_clients": "blocked_clients",
|
||||
"used_memory": "used_memory",
|
||||
"used_memory_rss": "used_memory_rss",
|
||||
"used_memory_peak": "used_memory_peak",
|
||||
"total_connections_received": "total_connections_received",
|
||||
"total_commands_processed": "total_commands_processed",
|
||||
"instantaneous_ops_per_sec": "instantaneous_ops_per_sec",
|
||||
"latest_fork_usec": "latest_fork_usec",
|
||||
"mem_fragmentation_ratio": "mem_fragmentation_ratio",
|
||||
"used_cpu_sys": "used_cpu_sys",
|
||||
"used_cpu_user": "used_cpu_user",
|
||||
"used_cpu_sys_children": "used_cpu_sys_children",
|
||||
"used_cpu_user_children": "used_cpu_user_children",
|
||||
"registered_jobs": "registered_jobs",
|
||||
"registered_queues": "registered_queues",
|
||||
}
|
||||
|
||||
var ErrProtocolError = errors.New("disque protocol error")
|
||||
|
||||
// Reads stats from all configured servers accumulates stats.
|
||||
// Returns one of the errors encountered while gather stats (if any).
|
||||
func (g *Disque) Gather(acc inputs.Accumulator) error {
|
||||
if len(g.Servers) == 0 {
|
||||
url := &url.URL{
|
||||
Host: ":7711",
|
||||
}
|
||||
g.gatherServer(url, acc)
|
||||
return nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var outerr error
|
||||
|
||||
for _, serv := range g.Servers {
|
||||
u, err := url.Parse(serv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse to address '%s': %s", serv, err)
|
||||
} else if u.Scheme == "" {
|
||||
// fallback to simple string based address (i.e. "10.0.0.1:10000")
|
||||
u.Scheme = "tcp"
|
||||
u.Host = serv
|
||||
u.Path = ""
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(serv string) {
|
||||
defer wg.Done()
|
||||
outerr = g.gatherServer(u, acc)
|
||||
}(serv)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
const defaultPort = "7711"
|
||||
|
||||
func (g *Disque) gatherServer(addr *url.URL, acc inputs.Accumulator) error {
|
||||
if g.c == nil {
|
||||
|
||||
_, _, err := net.SplitHostPort(addr.Host)
|
||||
if err != nil {
|
||||
addr.Host = addr.Host + ":" + defaultPort
|
||||
}
|
||||
|
||||
c, err := net.Dial("tcp", addr.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to connect to disque server '%s': %s", addr.Host, err)
|
||||
}
|
||||
|
||||
if addr.User != nil {
|
||||
pwd, set := addr.User.Password()
|
||||
if set && pwd != "" {
|
||||
c.Write([]byte(fmt.Sprintf("AUTH %s\r\n", pwd)))
|
||||
|
||||
r := bufio.NewReader(c)
|
||||
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if line[0] != '+' {
|
||||
return fmt.Errorf("%s", strings.TrimSpace(line)[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.c = c
|
||||
}
|
||||
|
||||
g.c.Write([]byte("info\r\n"))
|
||||
|
||||
r := bufio.NewReader(g.c)
|
||||
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if line[0] != '$' {
|
||||
return fmt.Errorf("bad line start: %s", ErrProtocolError)
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
szStr := line[1:]
|
||||
|
||||
sz, err := strconv.Atoi(szStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad size string <<%s>>: %s", szStr, ErrProtocolError)
|
||||
}
|
||||
|
||||
var read int
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
tags := map[string]string{"host": addr.String()}
|
||||
for read < sz {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
read += len(line)
|
||||
|
||||
if len(line) == 1 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
|
||||
name := string(parts[0])
|
||||
|
||||
metric, ok := Tracking[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
val := strings.TrimSpace(parts[1])
|
||||
|
||||
ival, err := strconv.ParseUint(val, 10, 64)
|
||||
if err == nil {
|
||||
fields[metric] = ival
|
||||
continue
|
||||
}
|
||||
|
||||
fval, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields[metric] = fval
|
||||
}
|
||||
acc.AddFields("disque", fields, tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("disque", func() inputs.Input {
|
||||
return &Disque{}
|
||||
})
|
||||
}
|
||||
217
plugins/inputs/disque/disque_test.go
Normal file
217
plugins/inputs/disque/disque_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package disque
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDisqueGeneratesMetrics(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer l.Close()
|
||||
|
||||
go func() {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf := bufio.NewReader(c)
|
||||
|
||||
for {
|
||||
line, err := buf.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if line != "info\r\n" {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(c, "$%d\n", len(testOutput))
|
||||
c.Write([]byte(testOutput))
|
||||
}
|
||||
}()
|
||||
|
||||
addr := fmt.Sprintf("disque://%s", l.Addr().String())
|
||||
|
||||
r := &Disque{
|
||||
Servers: []string{addr},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err = r.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"uptime": uint64(1452705),
|
||||
"clients": uint64(31),
|
||||
"blocked_clients": uint64(13),
|
||||
"used_memory": uint64(1840104),
|
||||
"used_memory_rss": uint64(3227648),
|
||||
"used_memory_peak": uint64(89603656),
|
||||
"total_connections_received": uint64(5062777),
|
||||
"total_commands_processed": uint64(12308396),
|
||||
"instantaneous_ops_per_sec": uint64(18),
|
||||
"latest_fork_usec": uint64(1644),
|
||||
"registered_jobs": uint64(360),
|
||||
"registered_queues": uint64(12),
|
||||
"mem_fragmentation_ratio": float64(1.75),
|
||||
"used_cpu_sys": float64(19585.73),
|
||||
"used_cpu_user": float64(11255.96),
|
||||
"used_cpu_sys_children": float64(1.75),
|
||||
"used_cpu_user_children": float64(1.91),
|
||||
}
|
||||
acc.AssertContainsFields(t, "disque", fields)
|
||||
}
|
||||
|
||||
func TestDisqueCanPullStatsFromMultipleServers(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer l.Close()
|
||||
|
||||
go func() {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf := bufio.NewReader(c)
|
||||
|
||||
for {
|
||||
line, err := buf.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if line != "info\r\n" {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(c, "$%d\n", len(testOutput))
|
||||
c.Write([]byte(testOutput))
|
||||
}
|
||||
}()
|
||||
|
||||
addr := fmt.Sprintf("disque://%s", l.Addr().String())
|
||||
|
||||
r := &Disque{
|
||||
Servers: []string{addr},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err = r.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"uptime": uint64(1452705),
|
||||
"clients": uint64(31),
|
||||
"blocked_clients": uint64(13),
|
||||
"used_memory": uint64(1840104),
|
||||
"used_memory_rss": uint64(3227648),
|
||||
"used_memory_peak": uint64(89603656),
|
||||
"total_connections_received": uint64(5062777),
|
||||
"total_commands_processed": uint64(12308396),
|
||||
"instantaneous_ops_per_sec": uint64(18),
|
||||
"latest_fork_usec": uint64(1644),
|
||||
"registered_jobs": uint64(360),
|
||||
"registered_queues": uint64(12),
|
||||
"mem_fragmentation_ratio": float64(1.75),
|
||||
"used_cpu_sys": float64(19585.73),
|
||||
"used_cpu_user": float64(11255.96),
|
||||
"used_cpu_sys_children": float64(1.75),
|
||||
"used_cpu_user_children": float64(1.91),
|
||||
}
|
||||
acc.AssertContainsFields(t, "disque", fields)
|
||||
}
|
||||
|
||||
const testOutput = `# Server
|
||||
disque_version:0.0.1
|
||||
disque_git_sha1:b5247598
|
||||
disque_git_dirty:0
|
||||
disque_build_id:379fda78983a60c6
|
||||
os:Linux 3.13.0-44-generic x86_64
|
||||
arch_bits:64
|
||||
multiplexing_api:epoll
|
||||
gcc_version:4.8.2
|
||||
process_id:32420
|
||||
run_id:1cfdfa4c6bc3f285182db5427522a8a4c16e42e4
|
||||
tcp_port:7711
|
||||
uptime_in_seconds:1452705
|
||||
uptime_in_days:16
|
||||
hz:10
|
||||
config_file:/usr/local/etc/disque/disque.conf
|
||||
|
||||
# Clients
|
||||
connected_clients:31
|
||||
client_longest_output_list:0
|
||||
client_biggest_input_buf:0
|
||||
blocked_clients:13
|
||||
|
||||
# Memory
|
||||
used_memory:1840104
|
||||
used_memory_human:1.75M
|
||||
used_memory_rss:3227648
|
||||
used_memory_peak:89603656
|
||||
used_memory_peak_human:85.45M
|
||||
mem_fragmentation_ratio:1.75
|
||||
mem_allocator:jemalloc-3.6.0
|
||||
|
||||
# Jobs
|
||||
registered_jobs:360
|
||||
|
||||
# Queues
|
||||
registered_queues:12
|
||||
|
||||
# Persistence
|
||||
loading:0
|
||||
aof_enabled:1
|
||||
aof_state:on
|
||||
aof_rewrite_in_progress:0
|
||||
aof_rewrite_scheduled:0
|
||||
aof_last_rewrite_time_sec:0
|
||||
aof_current_rewrite_time_sec:-1
|
||||
aof_last_bgrewrite_status:ok
|
||||
aof_last_write_status:ok
|
||||
aof_current_size:41952430
|
||||
aof_base_size:9808
|
||||
aof_pending_rewrite:0
|
||||
aof_buffer_length:0
|
||||
aof_rewrite_buffer_length:0
|
||||
aof_pending_bio_fsync:0
|
||||
aof_delayed_fsync:1
|
||||
|
||||
# Stats
|
||||
total_connections_received:5062777
|
||||
total_commands_processed:12308396
|
||||
instantaneous_ops_per_sec:18
|
||||
total_net_input_bytes:1346996528
|
||||
total_net_output_bytes:1967551763
|
||||
instantaneous_input_kbps:1.38
|
||||
instantaneous_output_kbps:1.78
|
||||
rejected_connections:0
|
||||
latest_fork_usec:1644
|
||||
|
||||
# CPU
|
||||
used_cpu_sys:19585.73
|
||||
used_cpu_user:11255.96
|
||||
used_cpu_sys_children:1.75
|
||||
used_cpu_user_children:1.91
|
||||
`
|
||||
320
plugins/inputs/elasticsearch/README.md
Normal file
320
plugins/inputs/elasticsearch/README.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# Elasticsearch plugin
|
||||
|
||||
#### Plugin arguments:
|
||||
- **servers** []string: list of one or more Elasticsearch servers
|
||||
- **local** boolean: If false, it will read the indices stats from all nodes
|
||||
- **cluster_health** boolean: If true, it will also obtain cluster level stats
|
||||
|
||||
#### Description
|
||||
|
||||
The [elasticsearch](https://www.elastic.co/) plugin queries endpoints to obtain
|
||||
[node](https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-stats.html)
|
||||
and optionally [cluster](https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html) stats.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
[elasticsearch]
|
||||
|
||||
servers = ["http://localhost:9200"]
|
||||
|
||||
local = true
|
||||
|
||||
cluster_health = true
|
||||
```
|
||||
|
||||
# Measurements
|
||||
#### cluster measurements (utilizes fields instead of single values):
|
||||
|
||||
contains `status`, `timed_out`, `number_of_nodes`, `number_of_data_nodes`,
|
||||
`active_primary_shards`, `active_shards`, `relocating_shards`,
|
||||
`initializing_shards`, `unassigned_shards` fields
|
||||
- elasticsearch_cluster_health
|
||||
|
||||
contains `status`, `number_of_shards`, `number_of_replicas`,
|
||||
`active_primary_shards`, `active_shards`, `relocating_shards`,
|
||||
`initializing_shards`, `unassigned_shards` fields
|
||||
- elasticsearch_indices
|
||||
|
||||
#### node measurements:
|
||||
|
||||
field data circuit breaker measurement names:
|
||||
- elasticsearch_breakers_fielddata_estimated_size_in_bytes value=0
|
||||
- elasticsearch_breakers_fielddata_overhead value=1.03
|
||||
- elasticsearch_breakers_fielddata_tripped value=0
|
||||
- elasticsearch_breakers_fielddata_limit_size_in_bytes value=623326003
|
||||
- elasticsearch_breakers_request_estimated_size_in_bytes value=0
|
||||
- elasticsearch_breakers_request_overhead value=1.0
|
||||
- elasticsearch_breakers_request_tripped value=0
|
||||
- elasticsearch_breakers_request_limit_size_in_bytes value=415550668
|
||||
- elasticsearch_breakers_parent_overhead value=1.0
|
||||
- elasticsearch_breakers_parent_tripped value=0
|
||||
- elasticsearch_breakers_parent_limit_size_in_bytes value=727213670
|
||||
- elasticsearch_breakers_parent_estimated_size_in_bytes value=0
|
||||
|
||||
File system information, data path, free disk space, read/write measurement names:
|
||||
- elasticsearch_fs_timestamp value=1436460392946
|
||||
- elasticsearch_fs_total_free_in_bytes value=16909316096
|
||||
- elasticsearch_fs_total_available_in_bytes value=15894814720
|
||||
- elasticsearch_fs_total_total_in_bytes value=19507089408
|
||||
|
||||
indices size, document count, indexing and deletion times, search times,
|
||||
field cache size, merges and flushes measurement names:
|
||||
- elasticsearch_indices_id_cache_memory_size_in_bytes value=0
|
||||
- elasticsearch_indices_completion_size_in_bytes value=0
|
||||
- elasticsearch_indices_suggest_total value=0
|
||||
- elasticsearch_indices_suggest_time_in_millis value=0
|
||||
- elasticsearch_indices_suggest_current value=0
|
||||
- elasticsearch_indices_query_cache_memory_size_in_bytes value=0
|
||||
- elasticsearch_indices_query_cache_evictions value=0
|
||||
- elasticsearch_indices_query_cache_hit_count value=0
|
||||
- elasticsearch_indices_query_cache_miss_count value=0
|
||||
- elasticsearch_indices_store_size_in_bytes value=37715234
|
||||
- elasticsearch_indices_store_throttle_time_in_millis value=215
|
||||
- elasticsearch_indices_merges_current_docs value=0
|
||||
- elasticsearch_indices_merges_current_size_in_bytes value=0
|
||||
- elasticsearch_indices_merges_total value=133
|
||||
- elasticsearch_indices_merges_total_time_in_millis value=21060
|
||||
- elasticsearch_indices_merges_total_docs value=203672
|
||||
- elasticsearch_indices_merges_total_size_in_bytes value=142900226
|
||||
- elasticsearch_indices_merges_current value=0
|
||||
- elasticsearch_indices_filter_cache_memory_size_in_bytes value=7384
|
||||
- elasticsearch_indices_filter_cache_evictions value=0
|
||||
- elasticsearch_indices_indexing_index_total value=84790
|
||||
- elasticsearch_indices_indexing_index_time_in_millis value=29680
|
||||
- elasticsearch_indices_indexing_index_current value=0
|
||||
- elasticsearch_indices_indexing_noop_update_total value=0
|
||||
- elasticsearch_indices_indexing_throttle_time_in_millis value=0
|
||||
- elasticsearch_indices_indexing_delete_tota value=13879
|
||||
- elasticsearch_indices_indexing_delete_time_in_millis value=1139
|
||||
- elasticsearch_indices_indexing_delete_current value=0
|
||||
- elasticsearch_indices_get_exists_time_in_millis value=0
|
||||
- elasticsearch_indices_get_missing_total value=1
|
||||
- elasticsearch_indices_get_missing_time_in_millis value=2
|
||||
- elasticsearch_indices_get_current value=0
|
||||
- elasticsearch_indices_get_total value=1
|
||||
- elasticsearch_indices_get_time_in_millis value=2
|
||||
- elasticsearch_indices_get_exists_total value=0
|
||||
- elasticsearch_indices_refresh_total value=1076
|
||||
- elasticsearch_indices_refresh_total_time_in_millis value=20078
|
||||
- elasticsearch_indices_percolate_current value=0
|
||||
- elasticsearch_indices_percolate_memory_size_in_bytes value=-1
|
||||
- elasticsearch_indices_percolate_queries value=0
|
||||
- elasticsearch_indices_percolate_total value=0
|
||||
- elasticsearch_indices_percolate_time_in_millis value=0
|
||||
- elasticsearch_indices_translog_operations value=17702
|
||||
- elasticsearch_indices_translog_size_in_bytes value=17
|
||||
- elasticsearch_indices_recovery_current_as_source value=0
|
||||
- elasticsearch_indices_recovery_current_as_target value=0
|
||||
- elasticsearch_indices_recovery_throttle_time_in_millis value=0
|
||||
- elasticsearch_indices_docs_count value=29652
|
||||
- elasticsearch_indices_docs_deleted value=5229
|
||||
- elasticsearch_indices_flush_total_time_in_millis value=2401
|
||||
- elasticsearch_indices_flush_total value=115
|
||||
- elasticsearch_indices_fielddata_memory_size_in_bytes value=12996
|
||||
- elasticsearch_indices_fielddata_evictions value=0
|
||||
- elasticsearch_indices_search_fetch_current value=0
|
||||
- elasticsearch_indices_search_open_contexts value=0
|
||||
- elasticsearch_indices_search_query_total value=1452
|
||||
- elasticsearch_indices_search_query_time_in_millis value=5695
|
||||
- elasticsearch_indices_search_query_current value=0
|
||||
- elasticsearch_indices_search_fetch_total value=414
|
||||
- elasticsearch_indices_search_fetch_time_in_millis value=146
|
||||
- elasticsearch_indices_warmer_current value=0
|
||||
- elasticsearch_indices_warmer_total value=2319
|
||||
- elasticsearch_indices_warmer_total_time_in_millis value=448
|
||||
- elasticsearch_indices_segments_count value=134
|
||||
- elasticsearch_indices_segments_memory_in_bytes value=1285212
|
||||
- elasticsearch_indices_segments_index_writer_memory_in_bytes value=0
|
||||
- elasticsearch_indices_segments_index_writer_max_memory_in_bytes value=172368955
|
||||
- elasticsearch_indices_segments_version_map_memory_in_bytes value=611844
|
||||
- elasticsearch_indices_segments_fixed_bit_set_memory_in_bytes value=0
|
||||
|
||||
HTTP connection measurement names:
|
||||
- elasticsearch_http_current_open value=3
|
||||
- elasticsearch_http_total_opened value=3
|
||||
|
||||
JVM stats, memory pool information, garbage collection, buffer pools measurement names:
|
||||
- elasticsearch_jvm_timestamp value=1436460392945
|
||||
- elasticsearch_jvm_uptime_in_millis value=202245
|
||||
- elasticsearch_jvm_mem_non_heap_used_in_bytes value=39634576
|
||||
- elasticsearch_jvm_mem_non_heap_committed_in_bytes value=40841216
|
||||
- elasticsearch_jvm_mem_pools_young_max_in_bytes value=279183360
|
||||
- elasticsearch_jvm_mem_pools_young_peak_used_in_bytes value=71630848
|
||||
- elasticsearch_jvm_mem_pools_young_peak_max_in_bytes value=279183360
|
||||
- elasticsearch_jvm_mem_pools_young_used_in_bytes value=32685760
|
||||
- elasticsearch_jvm_mem_pools_survivor_peak_used_in_bytes value=8912888
|
||||
- elasticsearch_jvm_mem_pools_survivor_peak_max_in_bytes value=34865152
|
||||
- elasticsearch_jvm_mem_pools_survivor_used_in_bytes value=8912880
|
||||
- elasticsearch_jvm_mem_pools_survivor_max_in_bytes value=34865152
|
||||
- elasticsearch_jvm_mem_pools_old_peak_max_in_bytes value=724828160
|
||||
- elasticsearch_jvm_mem_pools_old_used_in_bytes value=11110928
|
||||
- elasticsearch_jvm_mem_pools_old_max_in_bytes value=724828160
|
||||
- elasticsearch_jvm_mem_pools_old_peak_used_in_bytes value=14354608
|
||||
- elasticsearch_jvm_mem_heap_used_in_bytes value=52709568
|
||||
- elasticsearch_jvm_mem_heap_used_percent value=5
|
||||
- elasticsearch_jvm_mem_heap_committed_in_bytes value=259522560
|
||||
- elasticsearch_jvm_mem_heap_max_in_bytes value=1038876672
|
||||
- elasticsearch_jvm_threads_peak_count value=45
|
||||
- elasticsearch_jvm_threads_count value=44
|
||||
- elasticsearch_jvm_gc_collectors_young_collection_count value=2
|
||||
- elasticsearch_jvm_gc_collectors_young_collection_time_in_millis value=98
|
||||
- elasticsearch_jvm_gc_collectors_old_collection_count value=1
|
||||
- elasticsearch_jvm_gc_collectors_old_collection_time_in_millis value=24
|
||||
- elasticsearch_jvm_buffer_pools_direct_count value=40
|
||||
- elasticsearch_jvm_buffer_pools_direct_used_in_bytes value=6304239
|
||||
- elasticsearch_jvm_buffer_pools_direct_total_capacity_in_bytes value=6304239
|
||||
- elasticsearch_jvm_buffer_pools_mapped_count value=0
|
||||
- elasticsearch_jvm_buffer_pools_mapped_used_in_bytes value=0
|
||||
- elasticsearch_jvm_buffer_pools_mapped_total_capacity_in_bytes value=0
|
||||
|
||||
TCP information measurement names:
|
||||
- elasticsearch_network_tcp_in_errs value=0
|
||||
- elasticsearch_network_tcp_passive_opens value=16
|
||||
- elasticsearch_network_tcp_curr_estab value=29
|
||||
- elasticsearch_network_tcp_in_segs value=113
|
||||
- elasticsearch_network_tcp_out_segs value=97
|
||||
- elasticsearch_network_tcp_retrans_segs value=0
|
||||
- elasticsearch_network_tcp_attempt_fails value=0
|
||||
- elasticsearch_network_tcp_active_opens value=13
|
||||
- elasticsearch_network_tcp_estab_resets value=0
|
||||
- elasticsearch_network_tcp_out_rsts value=0
|
||||
|
||||
Operating system stats, load average, cpu, mem, swap measurement names:
|
||||
- elasticsearch_os_swap_used_in_bytes value=0
|
||||
- elasticsearch_os_swap_free_in_bytes value=487997440
|
||||
- elasticsearch_os_timestamp value=1436460392944
|
||||
- elasticsearch_os_uptime_in_millis value=25092
|
||||
- elasticsearch_os_cpu_sys value=0
|
||||
- elasticsearch_os_cpu_user value=0
|
||||
- elasticsearch_os_cpu_idle value=99
|
||||
- elasticsearch_os_cpu_usage value=0
|
||||
- elasticsearch_os_cpu_stolen value=0
|
||||
- elasticsearch_os_mem_free_percent value=74
|
||||
- elasticsearch_os_mem_used_percent value=25
|
||||
- elasticsearch_os_mem_actual_free_in_bytes value=1565470720
|
||||
- elasticsearch_os_mem_actual_used_in_bytes value=534159360
|
||||
- elasticsearch_os_mem_free_in_bytes value=477761536
|
||||
- elasticsearch_os_mem_used_in_bytes value=1621868544
|
||||
|
||||
Process statistics, memory consumption, cpu usage, open file descriptors measurement names:
|
||||
- elasticsearch_process_mem_resident_in_bytes value=246382592
|
||||
- elasticsearch_process_mem_share_in_bytes value=18747392
|
||||
- elasticsearch_process_mem_total_virtual_in_bytes value=4747890688
|
||||
- elasticsearch_process_timestamp value=1436460392945
|
||||
- elasticsearch_process_open_file_descriptors value=160
|
||||
- elasticsearch_process_cpu_total_in_millis value=15480
|
||||
- elasticsearch_process_cpu_percent value=2
|
||||
- elasticsearch_process_cpu_sys_in_millis value=1870
|
||||
- elasticsearch_process_cpu_user_in_millis value=13610
|
||||
|
||||
Statistics about each thread pool, including current size, queue and rejected tasks measurement names:
|
||||
- elasticsearch_thread_pool_merge_threads value=6
|
||||
- elasticsearch_thread_pool_merge_queue value=4
|
||||
- elasticsearch_thread_pool_merge_active value=5
|
||||
- elasticsearch_thread_pool_merge_rejected value=2
|
||||
- elasticsearch_thread_pool_merge_largest value=5
|
||||
- elasticsearch_thread_pool_merge_completed value=1
|
||||
- elasticsearch_thread_pool_bulk_threads value=4
|
||||
- elasticsearch_thread_pool_bulk_queue value=5
|
||||
- elasticsearch_thread_pool_bulk_active value=7
|
||||
- elasticsearch_thread_pool_bulk_rejected value=3
|
||||
- elasticsearch_thread_pool_bulk_largest value=1
|
||||
- elasticsearch_thread_pool_bulk_completed value=4
|
||||
- elasticsearch_thread_pool_warmer_threads value=2
|
||||
- elasticsearch_thread_pool_warmer_queue value=7
|
||||
- elasticsearch_thread_pool_warmer_active value=3
|
||||
- elasticsearch_thread_pool_warmer_rejected value=2
|
||||
- elasticsearch_thread_pool_warmer_largest value=3
|
||||
- elasticsearch_thread_pool_warmer_completed value=1
|
||||
- elasticsearch_thread_pool_get_largest value=2
|
||||
- elasticsearch_thread_pool_get_completed value=1
|
||||
- elasticsearch_thread_pool_get_threads value=1
|
||||
- elasticsearch_thread_pool_get_queue value=8
|
||||
- elasticsearch_thread_pool_get_active value=4
|
||||
- elasticsearch_thread_pool_get_rejected value=3
|
||||
- elasticsearch_thread_pool_index_threads value=6
|
||||
- elasticsearch_thread_pool_index_queue value=8
|
||||
- elasticsearch_thread_pool_index_active value=4
|
||||
- elasticsearch_thread_pool_index_rejected value=2
|
||||
- elasticsearch_thread_pool_index_largest value=3
|
||||
- elasticsearch_thread_pool_index_completed value=6
|
||||
- elasticsearch_thread_pool_suggest_threads value=2
|
||||
- elasticsearch_thread_pool_suggest_queue value=7
|
||||
- elasticsearch_thread_pool_suggest_active value=2
|
||||
- elasticsearch_thread_pool_suggest_rejected value=1
|
||||
- elasticsearch_thread_pool_suggest_largest value=8
|
||||
- elasticsearch_thread_pool_suggest_completed value=3
|
||||
- elasticsearch_thread_pool_fetch_shard_store_queue value=7
|
||||
- elasticsearch_thread_pool_fetch_shard_store_active value=4
|
||||
- elasticsearch_thread_pool_fetch_shard_store_rejected value=2
|
||||
- elasticsearch_thread_pool_fetch_shard_store_largest value=4
|
||||
- elasticsearch_thread_pool_fetch_shard_store_completed value=1
|
||||
- elasticsearch_thread_pool_fetch_shard_store_threads value=1
|
||||
- elasticsearch_thread_pool_management_threads value=2
|
||||
- elasticsearch_thread_pool_management_queue value=3
|
||||
- elasticsearch_thread_pool_management_active value=1
|
||||
- elasticsearch_thread_pool_management_rejected value=6
|
||||
- elasticsearch_thread_pool_management_largest value=2
|
||||
- elasticsearch_thread_pool_management_completed value=22
|
||||
- elasticsearch_thread_pool_percolate_queue value=23
|
||||
- elasticsearch_thread_pool_percolate_active value=13
|
||||
- elasticsearch_thread_pool_percolate_rejected value=235
|
||||
- elasticsearch_thread_pool_percolate_largest value=23
|
||||
- elasticsearch_thread_pool_percolate_completed value=33
|
||||
- elasticsearch_thread_pool_percolate_threads value=123
|
||||
- elasticsearch_thread_pool_listener_active value=4
|
||||
- elasticsearch_thread_pool_listener_rejected value=8
|
||||
- elasticsearch_thread_pool_listener_largest value=1
|
||||
- elasticsearch_thread_pool_listener_completed value=1
|
||||
- elasticsearch_thread_pool_listener_threads value=1
|
||||
- elasticsearch_thread_pool_listener_queue value=2
|
||||
- elasticsearch_thread_pool_search_rejected value=7
|
||||
- elasticsearch_thread_pool_search_largest value=2
|
||||
- elasticsearch_thread_pool_search_completed value=4
|
||||
- elasticsearch_thread_pool_search_threads value=5
|
||||
- elasticsearch_thread_pool_search_queue value=7
|
||||
- elasticsearch_thread_pool_search_active value=2
|
||||
- elasticsearch_thread_pool_fetch_shard_started_threads value=3
|
||||
- elasticsearch_thread_pool_fetch_shard_started_queue value=1
|
||||
- elasticsearch_thread_pool_fetch_shard_started_active value=5
|
||||
- elasticsearch_thread_pool_fetch_shard_started_rejected value=6
|
||||
- elasticsearch_thread_pool_fetch_shard_started_largest value=4
|
||||
- elasticsearch_thread_pool_fetch_shard_started_completed value=54
|
||||
- elasticsearch_thread_pool_refresh_rejected value=4
|
||||
- elasticsearch_thread_pool_refresh_largest value=8
|
||||
- elasticsearch_thread_pool_refresh_completed value=3
|
||||
- elasticsearch_thread_pool_refresh_threads value=23
|
||||
- elasticsearch_thread_pool_refresh_queue value=7
|
||||
- elasticsearch_thread_pool_refresh_active value=3
|
||||
- elasticsearch_thread_pool_optimize_threads value=3
|
||||
- elasticsearch_thread_pool_optimize_queue value=4
|
||||
- elasticsearch_thread_pool_optimize_active value=1
|
||||
- elasticsearch_thread_pool_optimize_rejected value=2
|
||||
- elasticsearch_thread_pool_optimize_largest value=7
|
||||
- elasticsearch_thread_pool_optimize_completed value=3
|
||||
- elasticsearch_thread_pool_snapshot_largest value=1
|
||||
- elasticsearch_thread_pool_snapshot_completed value=0
|
||||
- elasticsearch_thread_pool_snapshot_threads value=8
|
||||
- elasticsearch_thread_pool_snapshot_queue value=5
|
||||
- elasticsearch_thread_pool_snapshot_active value=6
|
||||
- elasticsearch_thread_pool_snapshot_rejected value=2
|
||||
- elasticsearch_thread_pool_generic_threads value=1
|
||||
- elasticsearch_thread_pool_generic_queue value=4
|
||||
- elasticsearch_thread_pool_generic_active value=6
|
||||
- elasticsearch_thread_pool_generic_rejected value=3
|
||||
- elasticsearch_thread_pool_generic_largest value=2
|
||||
- elasticsearch_thread_pool_generic_completed value=27
|
||||
- elasticsearch_thread_pool_flush_threads value=3
|
||||
- elasticsearch_thread_pool_flush_queue value=8
|
||||
- elasticsearch_thread_pool_flush_active value=0
|
||||
- elasticsearch_thread_pool_flush_rejected value=1
|
||||
- elasticsearch_thread_pool_flush_largest value=5
|
||||
- elasticsearch_thread_pool_flush_completed value=3
|
||||
|
||||
Transport statistics about sent and received bytes in cluster communication measurement names:
|
||||
- elasticsearch_transport_server_open value=13
|
||||
- elasticsearch_transport_rx_count value=6
|
||||
- elasticsearch_transport_rx_size_in_bytes value=1380
|
||||
- elasticsearch_transport_tx_count value=6
|
||||
- elasticsearch_transport_tx_size_in_bytes value=1380
|
||||
226
plugins/inputs/elasticsearch/elasticsearch.go
Normal file
226
plugins/inputs/elasticsearch/elasticsearch.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package elasticsearch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/internal"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
const statsPath = "/_nodes/stats"
|
||||
const statsPathLocal = "/_nodes/_local/stats"
|
||||
const healthPath = "/_cluster/health"
|
||||
|
||||
type node struct {
|
||||
Host string `json:"host"`
|
||||
Name string `json:"name"`
|
||||
Attributes map[string]string `json:"attributes"`
|
||||
Indices interface{} `json:"indices"`
|
||||
OS interface{} `json:"os"`
|
||||
Process interface{} `json:"process"`
|
||||
JVM interface{} `json:"jvm"`
|
||||
ThreadPool interface{} `json:"thread_pool"`
|
||||
FS interface{} `json:"fs"`
|
||||
Transport interface{} `json:"transport"`
|
||||
HTTP interface{} `json:"http"`
|
||||
Breakers interface{} `json:"breakers"`
|
||||
}
|
||||
|
||||
type clusterHealth struct {
|
||||
ClusterName string `json:"cluster_name"`
|
||||
Status string `json:"status"`
|
||||
TimedOut bool `json:"timed_out"`
|
||||
NumberOfNodes int `json:"number_of_nodes"`
|
||||
NumberOfDataNodes int `json:"number_of_data_nodes"`
|
||||
ActivePrimaryShards int `json:"active_primary_shards"`
|
||||
ActiveShards int `json:"active_shards"`
|
||||
RelocatingShards int `json:"relocating_shards"`
|
||||
InitializingShards int `json:"initializing_shards"`
|
||||
UnassignedShards int `json:"unassigned_shards"`
|
||||
Indices map[string]indexHealth `json:"indices"`
|
||||
}
|
||||
|
||||
type indexHealth struct {
|
||||
Status string `json:"status"`
|
||||
NumberOfShards int `json:"number_of_shards"`
|
||||
NumberOfReplicas int `json:"number_of_replicas"`
|
||||
ActivePrimaryShards int `json:"active_primary_shards"`
|
||||
ActiveShards int `json:"active_shards"`
|
||||
RelocatingShards int `json:"relocating_shards"`
|
||||
InitializingShards int `json:"initializing_shards"`
|
||||
UnassignedShards int `json:"unassigned_shards"`
|
||||
}
|
||||
|
||||
const sampleConfig = `
|
||||
# specify a list of one or more Elasticsearch servers
|
||||
servers = ["http://localhost:9200"]
|
||||
|
||||
# set local to false when you want to read the indices stats from all nodes
|
||||
# within the cluster
|
||||
local = true
|
||||
|
||||
# set cluster_health to true when you want to also obtain cluster level stats
|
||||
cluster_health = false
|
||||
`
|
||||
|
||||
// Elasticsearch is a plugin to read stats from one or many Elasticsearch
|
||||
// servers.
|
||||
type Elasticsearch struct {
|
||||
Local bool
|
||||
Servers []string
|
||||
ClusterHealth bool
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewElasticsearch return a new instance of Elasticsearch
|
||||
func NewElasticsearch() *Elasticsearch {
|
||||
return &Elasticsearch{client: http.DefaultClient}
|
||||
}
|
||||
|
||||
// SampleConfig returns sample configuration for this plugin.
|
||||
func (e *Elasticsearch) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
// Description returns the plugin description.
|
||||
func (e *Elasticsearch) Description() string {
|
||||
return "Read stats from one or more Elasticsearch servers or clusters"
|
||||
}
|
||||
|
||||
// Gather reads the stats from Elasticsearch and writes it to the
|
||||
// Accumulator.
|
||||
func (e *Elasticsearch) Gather(acc inputs.Accumulator) error {
|
||||
for _, serv := range e.Servers {
|
||||
var url string
|
||||
if e.Local {
|
||||
url = serv + statsPathLocal
|
||||
} else {
|
||||
url = serv + statsPath
|
||||
}
|
||||
if err := e.gatherNodeStats(url, acc); err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ClusterHealth {
|
||||
e.gatherClusterStats(fmt.Sprintf("%s/_cluster/health?level=indices", serv), acc)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Elasticsearch) gatherNodeStats(url string, acc inputs.Accumulator) error {
|
||||
nodeStats := &struct {
|
||||
ClusterName string `json:"cluster_name"`
|
||||
Nodes map[string]*node `json:"nodes"`
|
||||
}{}
|
||||
if err := e.gatherData(url, nodeStats); err != nil {
|
||||
return err
|
||||
}
|
||||
for id, n := range nodeStats.Nodes {
|
||||
tags := map[string]string{
|
||||
"node_id": id,
|
||||
"node_host": n.Host,
|
||||
"node_name": n.Name,
|
||||
"cluster_name": nodeStats.ClusterName,
|
||||
}
|
||||
|
||||
for k, v := range n.Attributes {
|
||||
tags["node_attribute_"+k] = v
|
||||
}
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"indices": n.Indices,
|
||||
"os": n.OS,
|
||||
"process": n.Process,
|
||||
"jvm": n.JVM,
|
||||
"thread_pool": n.ThreadPool,
|
||||
"fs": n.FS,
|
||||
"transport": n.Transport,
|
||||
"http": n.HTTP,
|
||||
"breakers": n.Breakers,
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for p, s := range stats {
|
||||
f := internal.JSONFlattener{}
|
||||
err := f.FlattenJSON("", s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
acc.AddFields("elasticsearch_"+p, f.Fields, tags, now)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Elasticsearch) gatherClusterStats(url string, acc inputs.Accumulator) error {
|
||||
clusterStats := &clusterHealth{}
|
||||
if err := e.gatherData(url, clusterStats); err != nil {
|
||||
return err
|
||||
}
|
||||
measurementTime := time.Now()
|
||||
clusterFields := map[string]interface{}{
|
||||
"status": clusterStats.Status,
|
||||
"timed_out": clusterStats.TimedOut,
|
||||
"number_of_nodes": clusterStats.NumberOfNodes,
|
||||
"number_of_data_nodes": clusterStats.NumberOfDataNodes,
|
||||
"active_primary_shards": clusterStats.ActivePrimaryShards,
|
||||
"active_shards": clusterStats.ActiveShards,
|
||||
"relocating_shards": clusterStats.RelocatingShards,
|
||||
"initializing_shards": clusterStats.InitializingShards,
|
||||
"unassigned_shards": clusterStats.UnassignedShards,
|
||||
}
|
||||
acc.AddFields(
|
||||
"elasticsearch_cluster_health",
|
||||
clusterFields,
|
||||
map[string]string{"name": clusterStats.ClusterName},
|
||||
measurementTime,
|
||||
)
|
||||
|
||||
for name, health := range clusterStats.Indices {
|
||||
indexFields := map[string]interface{}{
|
||||
"status": health.Status,
|
||||
"number_of_shards": health.NumberOfShards,
|
||||
"number_of_replicas": health.NumberOfReplicas,
|
||||
"active_primary_shards": health.ActivePrimaryShards,
|
||||
"active_shards": health.ActiveShards,
|
||||
"relocating_shards": health.RelocatingShards,
|
||||
"initializing_shards": health.InitializingShards,
|
||||
"unassigned_shards": health.UnassignedShards,
|
||||
}
|
||||
acc.AddFields(
|
||||
"elasticsearch_indices",
|
||||
indexFields,
|
||||
map[string]string{"index": name},
|
||||
measurementTime,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Elasticsearch) gatherData(url string, v interface{}) error {
|
||||
r, err := e.client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if r.StatusCode != http.StatusOK {
|
||||
// NOTE: we are not going to read/discard r.Body under the assumption we'd prefer
|
||||
// to let the underlying transport close the connection and re-establish a new one for
|
||||
// future calls.
|
||||
return fmt.Errorf("elasticsearch: API responded with status-code %d, expected %d",
|
||||
r.StatusCode, http.StatusOK)
|
||||
}
|
||||
if err = json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("elasticsearch", func() inputs.Input {
|
||||
return NewElasticsearch()
|
||||
})
|
||||
}
|
||||
86
plugins/inputs/elasticsearch/elasticsearch_test.go
Normal file
86
plugins/inputs/elasticsearch/elasticsearch_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package elasticsearch
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type transportMock struct {
|
||||
statusCode int
|
||||
body string
|
||||
}
|
||||
|
||||
func newTransportMock(statusCode int, body string) http.RoundTripper {
|
||||
return &transportMock{
|
||||
statusCode: statusCode,
|
||||
body: body,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *transportMock) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
res := &http.Response{
|
||||
Header: make(http.Header),
|
||||
Request: r,
|
||||
StatusCode: t.statusCode,
|
||||
}
|
||||
res.Header.Set("Content-Type", "application/json")
|
||||
res.Body = ioutil.NopCloser(strings.NewReader(t.body))
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func TestElasticsearch(t *testing.T) {
|
||||
es := NewElasticsearch()
|
||||
es.Servers = []string{"http://example.com:9200"}
|
||||
es.client.Transport = newTransportMock(http.StatusOK, statsResponse)
|
||||
|
||||
var acc testutil.Accumulator
|
||||
if err := es.Gather(&acc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"cluster_name": "es-testcluster",
|
||||
"node_attribute_master": "true",
|
||||
"node_id": "SDFsfSDFsdfFSDSDfSFDSDF",
|
||||
"node_name": "test.host.com",
|
||||
"node_host": "test",
|
||||
}
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_indices", indicesExpected, tags)
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_os", osExpected, tags)
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_process", processExpected, tags)
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_jvm", jvmExpected, tags)
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_thread_pool", threadPoolExpected, tags)
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_fs", fsExpected, tags)
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_transport", transportExpected, tags)
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_http", httpExpected, tags)
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_breakers", breakersExpected, tags)
|
||||
}
|
||||
|
||||
func TestGatherClusterStats(t *testing.T) {
|
||||
es := NewElasticsearch()
|
||||
es.Servers = []string{"http://example.com:9200"}
|
||||
es.ClusterHealth = true
|
||||
es.client.Transport = newTransportMock(http.StatusOK, clusterResponse)
|
||||
|
||||
var acc testutil.Accumulator
|
||||
require.NoError(t, es.Gather(&acc))
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_cluster_health",
|
||||
clusterHealthExpected,
|
||||
map[string]string{"name": "elasticsearch_telegraf"})
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_indices",
|
||||
v1IndexExpected,
|
||||
map[string]string{"index": "v1"})
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "elasticsearch_indices",
|
||||
v2IndexExpected,
|
||||
map[string]string{"index": "v2"})
|
||||
}
|
||||
759
plugins/inputs/elasticsearch/testdata_test.go
Normal file
759
plugins/inputs/elasticsearch/testdata_test.go
Normal file
@@ -0,0 +1,759 @@
|
||||
package elasticsearch
|
||||
|
||||
const clusterResponse = `
|
||||
{
|
||||
"cluster_name": "elasticsearch_telegraf",
|
||||
"status": "green",
|
||||
"timed_out": false,
|
||||
"number_of_nodes": 3,
|
||||
"number_of_data_nodes": 3,
|
||||
"active_primary_shards": 5,
|
||||
"active_shards": 15,
|
||||
"relocating_shards": 0,
|
||||
"initializing_shards": 0,
|
||||
"unassigned_shards": 0,
|
||||
"indices": {
|
||||
"v1": {
|
||||
"status": "green",
|
||||
"number_of_shards": 10,
|
||||
"number_of_replicas": 1,
|
||||
"active_primary_shards": 10,
|
||||
"active_shards": 20,
|
||||
"relocating_shards": 0,
|
||||
"initializing_shards": 0,
|
||||
"unassigned_shards": 0
|
||||
},
|
||||
"v2": {
|
||||
"status": "red",
|
||||
"number_of_shards": 10,
|
||||
"number_of_replicas": 1,
|
||||
"active_primary_shards": 0,
|
||||
"active_shards": 0,
|
||||
"relocating_shards": 0,
|
||||
"initializing_shards": 0,
|
||||
"unassigned_shards": 20
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var clusterHealthExpected = map[string]interface{}{
|
||||
"status": "green",
|
||||
"timed_out": false,
|
||||
"number_of_nodes": 3,
|
||||
"number_of_data_nodes": 3,
|
||||
"active_primary_shards": 5,
|
||||
"active_shards": 15,
|
||||
"relocating_shards": 0,
|
||||
"initializing_shards": 0,
|
||||
"unassigned_shards": 0,
|
||||
}
|
||||
|
||||
var v1IndexExpected = map[string]interface{}{
|
||||
"status": "green",
|
||||
"number_of_shards": 10,
|
||||
"number_of_replicas": 1,
|
||||
"active_primary_shards": 10,
|
||||
"active_shards": 20,
|
||||
"relocating_shards": 0,
|
||||
"initializing_shards": 0,
|
||||
"unassigned_shards": 0,
|
||||
}
|
||||
|
||||
var v2IndexExpected = map[string]interface{}{
|
||||
"status": "red",
|
||||
"number_of_shards": 10,
|
||||
"number_of_replicas": 1,
|
||||
"active_primary_shards": 0,
|
||||
"active_shards": 0,
|
||||
"relocating_shards": 0,
|
||||
"initializing_shards": 0,
|
||||
"unassigned_shards": 20,
|
||||
}
|
||||
|
||||
const statsResponse = `
|
||||
{
|
||||
"cluster_name": "es-testcluster",
|
||||
"nodes": {
|
||||
"SDFsfSDFsdfFSDSDfSFDSDF": {
|
||||
"timestamp": 1436365550135,
|
||||
"name": "test.host.com",
|
||||
"transport_address": "inet[/127.0.0.1:9300]",
|
||||
"host": "test",
|
||||
"ip": [
|
||||
"inet[/127.0.0.1:9300]",
|
||||
"NONE"
|
||||
],
|
||||
"attributes": {
|
||||
"master": "true"
|
||||
},
|
||||
"indices": {
|
||||
"docs": {
|
||||
"count": 29652,
|
||||
"deleted": 5229
|
||||
},
|
||||
"store": {
|
||||
"size_in_bytes": 37715234,
|
||||
"throttle_time_in_millis": 215
|
||||
},
|
||||
"indexing": {
|
||||
"index_total": 84790,
|
||||
"index_time_in_millis": 29680,
|
||||
"index_current": 0,
|
||||
"delete_total": 13879,
|
||||
"delete_time_in_millis": 1139,
|
||||
"delete_current": 0,
|
||||
"noop_update_total": 0,
|
||||
"is_throttled": false,
|
||||
"throttle_time_in_millis": 0
|
||||
},
|
||||
"get": {
|
||||
"total": 1,
|
||||
"time_in_millis": 2,
|
||||
"exists_total": 0,
|
||||
"exists_time_in_millis": 0,
|
||||
"missing_total": 1,
|
||||
"missing_time_in_millis": 2,
|
||||
"current": 0
|
||||
},
|
||||
"search": {
|
||||
"open_contexts": 0,
|
||||
"query_total": 1452,
|
||||
"query_time_in_millis": 5695,
|
||||
"query_current": 0,
|
||||
"fetch_total": 414,
|
||||
"fetch_time_in_millis": 146,
|
||||
"fetch_current": 0
|
||||
},
|
||||
"merges": {
|
||||
"current": 0,
|
||||
"current_docs": 0,
|
||||
"current_size_in_bytes": 0,
|
||||
"total": 133,
|
||||
"total_time_in_millis": 21060,
|
||||
"total_docs": 203672,
|
||||
"total_size_in_bytes": 142900226
|
||||
},
|
||||
"refresh": {
|
||||
"total": 1076,
|
||||
"total_time_in_millis": 20078
|
||||
},
|
||||
"flush": {
|
||||
"total": 115,
|
||||
"total_time_in_millis": 2401
|
||||
},
|
||||
"warmer": {
|
||||
"current": 0,
|
||||
"total": 2319,
|
||||
"total_time_in_millis": 448
|
||||
},
|
||||
"filter_cache": {
|
||||
"memory_size_in_bytes": 7384,
|
||||
"evictions": 0
|
||||
},
|
||||
"id_cache": {
|
||||
"memory_size_in_bytes": 0
|
||||
},
|
||||
"fielddata": {
|
||||
"memory_size_in_bytes": 12996,
|
||||
"evictions": 0
|
||||
},
|
||||
"percolate": {
|
||||
"total": 0,
|
||||
"time_in_millis": 0,
|
||||
"current": 0,
|
||||
"memory_size_in_bytes": -1,
|
||||
"memory_size": "-1b",
|
||||
"queries": 0
|
||||
},
|
||||
"completion": {
|
||||
"size_in_bytes": 0
|
||||
},
|
||||
"segments": {
|
||||
"count": 134,
|
||||
"memory_in_bytes": 1285212,
|
||||
"index_writer_memory_in_bytes": 0,
|
||||
"index_writer_max_memory_in_bytes": 172368955,
|
||||
"version_map_memory_in_bytes": 611844,
|
||||
"fixed_bit_set_memory_in_bytes": 0
|
||||
},
|
||||
"translog": {
|
||||
"operations": 17702,
|
||||
"size_in_bytes": 17
|
||||
},
|
||||
"suggest": {
|
||||
"total": 0,
|
||||
"time_in_millis": 0,
|
||||
"current": 0
|
||||
},
|
||||
"query_cache": {
|
||||
"memory_size_in_bytes": 0,
|
||||
"evictions": 0,
|
||||
"hit_count": 0,
|
||||
"miss_count": 0
|
||||
},
|
||||
"recovery": {
|
||||
"current_as_source": 0,
|
||||
"current_as_target": 0,
|
||||
"throttle_time_in_millis": 0
|
||||
}
|
||||
},
|
||||
"os": {
|
||||
"timestamp": 1436460392944,
|
||||
"load_average": [
|
||||
0.01,
|
||||
0.04,
|
||||
0.05
|
||||
],
|
||||
"mem": {
|
||||
"free_in_bytes": 477761536,
|
||||
"used_in_bytes": 1621868544,
|
||||
"free_percent": 74,
|
||||
"used_percent": 25,
|
||||
"actual_free_in_bytes": 1565470720,
|
||||
"actual_used_in_bytes": 534159360
|
||||
},
|
||||
"swap": {
|
||||
"used_in_bytes": 0,
|
||||
"free_in_bytes": 487997440
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"timestamp": 1436460392945,
|
||||
"open_file_descriptors": 160,
|
||||
"cpu": {
|
||||
"percent": 2,
|
||||
"sys_in_millis": 1870,
|
||||
"user_in_millis": 13610,
|
||||
"total_in_millis": 15480
|
||||
},
|
||||
"mem": {
|
||||
"total_virtual_in_bytes": 4747890688
|
||||
}
|
||||
},
|
||||
"jvm": {
|
||||
"timestamp": 1436460392945,
|
||||
"uptime_in_millis": 202245,
|
||||
"mem": {
|
||||
"heap_used_in_bytes": 52709568,
|
||||
"heap_used_percent": 5,
|
||||
"heap_committed_in_bytes": 259522560,
|
||||
"heap_max_in_bytes": 1038876672,
|
||||
"non_heap_used_in_bytes": 39634576,
|
||||
"non_heap_committed_in_bytes": 40841216,
|
||||
"pools": {
|
||||
"young": {
|
||||
"used_in_bytes": 32685760,
|
||||
"max_in_bytes": 279183360,
|
||||
"peak_used_in_bytes": 71630848,
|
||||
"peak_max_in_bytes": 279183360
|
||||
},
|
||||
"survivor": {
|
||||
"used_in_bytes": 8912880,
|
||||
"max_in_bytes": 34865152,
|
||||
"peak_used_in_bytes": 8912888,
|
||||
"peak_max_in_bytes": 34865152
|
||||
},
|
||||
"old": {
|
||||
"used_in_bytes": 11110928,
|
||||
"max_in_bytes": 724828160,
|
||||
"peak_used_in_bytes": 14354608,
|
||||
"peak_max_in_bytes": 724828160
|
||||
}
|
||||
}
|
||||
},
|
||||
"threads": {
|
||||
"count": 44,
|
||||
"peak_count": 45
|
||||
},
|
||||
"gc": {
|
||||
"collectors": {
|
||||
"young": {
|
||||
"collection_count": 2,
|
||||
"collection_time_in_millis": 98
|
||||
},
|
||||
"old": {
|
||||
"collection_count": 1,
|
||||
"collection_time_in_millis": 24
|
||||
}
|
||||
}
|
||||
},
|
||||
"buffer_pools": {
|
||||
"direct": {
|
||||
"count": 40,
|
||||
"used_in_bytes": 6304239,
|
||||
"total_capacity_in_bytes": 6304239
|
||||
},
|
||||
"mapped": {
|
||||
"count": 0,
|
||||
"used_in_bytes": 0,
|
||||
"total_capacity_in_bytes": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"thread_pool": {
|
||||
"percolate": {
|
||||
"threads": 123,
|
||||
"queue": 23,
|
||||
"active": 13,
|
||||
"rejected": 235,
|
||||
"largest": 23,
|
||||
"completed": 33
|
||||
},
|
||||
"fetch_shard_started": {
|
||||
"threads": 3,
|
||||
"queue": 1,
|
||||
"active": 5,
|
||||
"rejected": 6,
|
||||
"largest": 4,
|
||||
"completed": 54
|
||||
},
|
||||
"listener": {
|
||||
"threads": 1,
|
||||
"queue": 2,
|
||||
"active": 4,
|
||||
"rejected": 8,
|
||||
"largest": 1,
|
||||
"completed": 1
|
||||
},
|
||||
"index": {
|
||||
"threads": 6,
|
||||
"queue": 8,
|
||||
"active": 4,
|
||||
"rejected": 2,
|
||||
"largest": 3,
|
||||
"completed": 6
|
||||
},
|
||||
"refresh": {
|
||||
"threads": 23,
|
||||
"queue": 7,
|
||||
"active": 3,
|
||||
"rejected": 4,
|
||||
"largest": 8,
|
||||
"completed": 3
|
||||
},
|
||||
"suggest": {
|
||||
"threads": 2,
|
||||
"queue": 7,
|
||||
"active": 2,
|
||||
"rejected": 1,
|
||||
"largest": 8,
|
||||
"completed": 3
|
||||
},
|
||||
"generic": {
|
||||
"threads": 1,
|
||||
"queue": 4,
|
||||
"active": 6,
|
||||
"rejected": 3,
|
||||
"largest": 2,
|
||||
"completed": 27
|
||||
},
|
||||
"warmer": {
|
||||
"threads": 2,
|
||||
"queue": 7,
|
||||
"active": 3,
|
||||
"rejected": 2,
|
||||
"largest": 3,
|
||||
"completed": 1
|
||||
},
|
||||
"search": {
|
||||
"threads": 5,
|
||||
"queue": 7,
|
||||
"active": 2,
|
||||
"rejected": 7,
|
||||
"largest": 2,
|
||||
"completed": 4
|
||||
},
|
||||
"flush": {
|
||||
"threads": 3,
|
||||
"queue": 8,
|
||||
"active": 0,
|
||||
"rejected": 1,
|
||||
"largest": 5,
|
||||
"completed": 3
|
||||
},
|
||||
"optimize": {
|
||||
"threads": 3,
|
||||
"queue": 4,
|
||||
"active": 1,
|
||||
"rejected": 2,
|
||||
"largest": 7,
|
||||
"completed": 3
|
||||
},
|
||||
"fetch_shard_store": {
|
||||
"threads": 1,
|
||||
"queue": 7,
|
||||
"active": 4,
|
||||
"rejected": 2,
|
||||
"largest": 4,
|
||||
"completed": 1
|
||||
},
|
||||
"management": {
|
||||
"threads": 2,
|
||||
"queue": 3,
|
||||
"active": 1,
|
||||
"rejected": 6,
|
||||
"largest": 2,
|
||||
"completed": 22
|
||||
},
|
||||
"get": {
|
||||
"threads": 1,
|
||||
"queue": 8,
|
||||
"active": 4,
|
||||
"rejected": 3,
|
||||
"largest": 2,
|
||||
"completed": 1
|
||||
},
|
||||
"merge": {
|
||||
"threads": 6,
|
||||
"queue": 4,
|
||||
"active": 5,
|
||||
"rejected": 2,
|
||||
"largest": 5,
|
||||
"completed": 1
|
||||
},
|
||||
"bulk": {
|
||||
"threads": 4,
|
||||
"queue": 5,
|
||||
"active": 7,
|
||||
"rejected": 3,
|
||||
"largest": 1,
|
||||
"completed": 4
|
||||
},
|
||||
"snapshot": {
|
||||
"threads": 8,
|
||||
"queue": 5,
|
||||
"active": 6,
|
||||
"rejected": 2,
|
||||
"largest": 1,
|
||||
"completed": 0
|
||||
}
|
||||
},
|
||||
"fs": {
|
||||
"timestamp": 1436460392946,
|
||||
"total": {
|
||||
"total_in_bytes": 19507089408,
|
||||
"free_in_bytes": 16909316096,
|
||||
"available_in_bytes": 15894814720
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"path": "/usr/share/elasticsearch/data/elasticsearch/nodes/0",
|
||||
"mount": "/usr/share/elasticsearch/data",
|
||||
"type": "ext4",
|
||||
"total_in_bytes": 19507089408,
|
||||
"free_in_bytes": 16909316096,
|
||||
"available_in_bytes": 15894814720
|
||||
}
|
||||
]
|
||||
},
|
||||
"transport": {
|
||||
"server_open": 13,
|
||||
"rx_count": 6,
|
||||
"rx_size_in_bytes": 1380,
|
||||
"tx_count": 6,
|
||||
"tx_size_in_bytes": 1380
|
||||
},
|
||||
"http": {
|
||||
"current_open": 3,
|
||||
"total_opened": 3
|
||||
},
|
||||
"breakers": {
|
||||
"fielddata": {
|
||||
"limit_size_in_bytes": 623326003,
|
||||
"limit_size": "594.4mb",
|
||||
"estimated_size_in_bytes": 0,
|
||||
"estimated_size": "0b",
|
||||
"overhead": 1.03,
|
||||
"tripped": 0
|
||||
},
|
||||
"request": {
|
||||
"limit_size_in_bytes": 415550668,
|
||||
"limit_size": "396.2mb",
|
||||
"estimated_size_in_bytes": 0,
|
||||
"estimated_size": "0b",
|
||||
"overhead": 1.0,
|
||||
"tripped": 0
|
||||
},
|
||||
"parent": {
|
||||
"limit_size_in_bytes": 727213670,
|
||||
"limit_size": "693.5mb",
|
||||
"estimated_size_in_bytes": 0,
|
||||
"estimated_size": "0b",
|
||||
"overhead": 1.0,
|
||||
"tripped": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var indicesExpected = map[string]interface{}{
|
||||
"id_cache_memory_size_in_bytes": float64(0),
|
||||
"completion_size_in_bytes": float64(0),
|
||||
"suggest_total": float64(0),
|
||||
"suggest_time_in_millis": float64(0),
|
||||
"suggest_current": float64(0),
|
||||
"query_cache_memory_size_in_bytes": float64(0),
|
||||
"query_cache_evictions": float64(0),
|
||||
"query_cache_hit_count": float64(0),
|
||||
"query_cache_miss_count": float64(0),
|
||||
"store_size_in_bytes": float64(37715234),
|
||||
"store_throttle_time_in_millis": float64(215),
|
||||
"merges_current_docs": float64(0),
|
||||
"merges_current_size_in_bytes": float64(0),
|
||||
"merges_total": float64(133),
|
||||
"merges_total_time_in_millis": float64(21060),
|
||||
"merges_total_docs": float64(203672),
|
||||
"merges_total_size_in_bytes": float64(142900226),
|
||||
"merges_current": float64(0),
|
||||
"filter_cache_memory_size_in_bytes": float64(7384),
|
||||
"filter_cache_evictions": float64(0),
|
||||
"indexing_index_total": float64(84790),
|
||||
"indexing_index_time_in_millis": float64(29680),
|
||||
"indexing_index_current": float64(0),
|
||||
"indexing_noop_update_total": float64(0),
|
||||
"indexing_throttle_time_in_millis": float64(0),
|
||||
"indexing_delete_total": float64(13879),
|
||||
"indexing_delete_time_in_millis": float64(1139),
|
||||
"indexing_delete_current": float64(0),
|
||||
"get_exists_time_in_millis": float64(0),
|
||||
"get_missing_total": float64(1),
|
||||
"get_missing_time_in_millis": float64(2),
|
||||
"get_current": float64(0),
|
||||
"get_total": float64(1),
|
||||
"get_time_in_millis": float64(2),
|
||||
"get_exists_total": float64(0),
|
||||
"refresh_total": float64(1076),
|
||||
"refresh_total_time_in_millis": float64(20078),
|
||||
"percolate_current": float64(0),
|
||||
"percolate_memory_size_in_bytes": float64(-1),
|
||||
"percolate_queries": float64(0),
|
||||
"percolate_total": float64(0),
|
||||
"percolate_time_in_millis": float64(0),
|
||||
"translog_operations": float64(17702),
|
||||
"translog_size_in_bytes": float64(17),
|
||||
"recovery_current_as_source": float64(0),
|
||||
"recovery_current_as_target": float64(0),
|
||||
"recovery_throttle_time_in_millis": float64(0),
|
||||
"docs_count": float64(29652),
|
||||
"docs_deleted": float64(5229),
|
||||
"flush_total_time_in_millis": float64(2401),
|
||||
"flush_total": float64(115),
|
||||
"fielddata_memory_size_in_bytes": float64(12996),
|
||||
"fielddata_evictions": float64(0),
|
||||
"search_fetch_current": float64(0),
|
||||
"search_open_contexts": float64(0),
|
||||
"search_query_total": float64(1452),
|
||||
"search_query_time_in_millis": float64(5695),
|
||||
"search_query_current": float64(0),
|
||||
"search_fetch_total": float64(414),
|
||||
"search_fetch_time_in_millis": float64(146),
|
||||
"warmer_current": float64(0),
|
||||
"warmer_total": float64(2319),
|
||||
"warmer_total_time_in_millis": float64(448),
|
||||
"segments_count": float64(134),
|
||||
"segments_memory_in_bytes": float64(1285212),
|
||||
"segments_index_writer_memory_in_bytes": float64(0),
|
||||
"segments_index_writer_max_memory_in_bytes": float64(172368955),
|
||||
"segments_version_map_memory_in_bytes": float64(611844),
|
||||
"segments_fixed_bit_set_memory_in_bytes": float64(0),
|
||||
}
|
||||
|
||||
var osExpected = map[string]interface{}{
|
||||
"swap_used_in_bytes": float64(0),
|
||||
"swap_free_in_bytes": float64(487997440),
|
||||
"timestamp": float64(1436460392944),
|
||||
"mem_free_percent": float64(74),
|
||||
"mem_used_percent": float64(25),
|
||||
"mem_actual_free_in_bytes": float64(1565470720),
|
||||
"mem_actual_used_in_bytes": float64(534159360),
|
||||
"mem_free_in_bytes": float64(477761536),
|
||||
"mem_used_in_bytes": float64(1621868544),
|
||||
}
|
||||
|
||||
var processExpected = map[string]interface{}{
|
||||
"mem_total_virtual_in_bytes": float64(4747890688),
|
||||
"timestamp": float64(1436460392945),
|
||||
"open_file_descriptors": float64(160),
|
||||
"cpu_total_in_millis": float64(15480),
|
||||
"cpu_percent": float64(2),
|
||||
"cpu_sys_in_millis": float64(1870),
|
||||
"cpu_user_in_millis": float64(13610),
|
||||
}
|
||||
|
||||
var jvmExpected = map[string]interface{}{
|
||||
"timestamp": float64(1436460392945),
|
||||
"uptime_in_millis": float64(202245),
|
||||
"mem_non_heap_used_in_bytes": float64(39634576),
|
||||
"mem_non_heap_committed_in_bytes": float64(40841216),
|
||||
"mem_pools_young_max_in_bytes": float64(279183360),
|
||||
"mem_pools_young_peak_used_in_bytes": float64(71630848),
|
||||
"mem_pools_young_peak_max_in_bytes": float64(279183360),
|
||||
"mem_pools_young_used_in_bytes": float64(32685760),
|
||||
"mem_pools_survivor_peak_used_in_bytes": float64(8912888),
|
||||
"mem_pools_survivor_peak_max_in_bytes": float64(34865152),
|
||||
"mem_pools_survivor_used_in_bytes": float64(8912880),
|
||||
"mem_pools_survivor_max_in_bytes": float64(34865152),
|
||||
"mem_pools_old_peak_max_in_bytes": float64(724828160),
|
||||
"mem_pools_old_used_in_bytes": float64(11110928),
|
||||
"mem_pools_old_max_in_bytes": float64(724828160),
|
||||
"mem_pools_old_peak_used_in_bytes": float64(14354608),
|
||||
"mem_heap_used_in_bytes": float64(52709568),
|
||||
"mem_heap_used_percent": float64(5),
|
||||
"mem_heap_committed_in_bytes": float64(259522560),
|
||||
"mem_heap_max_in_bytes": float64(1038876672),
|
||||
"threads_peak_count": float64(45),
|
||||
"threads_count": float64(44),
|
||||
"gc_collectors_young_collection_count": float64(2),
|
||||
"gc_collectors_young_collection_time_in_millis": float64(98),
|
||||
"gc_collectors_old_collection_count": float64(1),
|
||||
"gc_collectors_old_collection_time_in_millis": float64(24),
|
||||
"buffer_pools_direct_count": float64(40),
|
||||
"buffer_pools_direct_used_in_bytes": float64(6304239),
|
||||
"buffer_pools_direct_total_capacity_in_bytes": float64(6304239),
|
||||
"buffer_pools_mapped_count": float64(0),
|
||||
"buffer_pools_mapped_used_in_bytes": float64(0),
|
||||
"buffer_pools_mapped_total_capacity_in_bytes": float64(0),
|
||||
}
|
||||
|
||||
var threadPoolExpected = map[string]interface{}{
|
||||
"merge_threads": float64(6),
|
||||
"merge_queue": float64(4),
|
||||
"merge_active": float64(5),
|
||||
"merge_rejected": float64(2),
|
||||
"merge_largest": float64(5),
|
||||
"merge_completed": float64(1),
|
||||
"bulk_threads": float64(4),
|
||||
"bulk_queue": float64(5),
|
||||
"bulk_active": float64(7),
|
||||
"bulk_rejected": float64(3),
|
||||
"bulk_largest": float64(1),
|
||||
"bulk_completed": float64(4),
|
||||
"warmer_threads": float64(2),
|
||||
"warmer_queue": float64(7),
|
||||
"warmer_active": float64(3),
|
||||
"warmer_rejected": float64(2),
|
||||
"warmer_largest": float64(3),
|
||||
"warmer_completed": float64(1),
|
||||
"get_largest": float64(2),
|
||||
"get_completed": float64(1),
|
||||
"get_threads": float64(1),
|
||||
"get_queue": float64(8),
|
||||
"get_active": float64(4),
|
||||
"get_rejected": float64(3),
|
||||
"index_threads": float64(6),
|
||||
"index_queue": float64(8),
|
||||
"index_active": float64(4),
|
||||
"index_rejected": float64(2),
|
||||
"index_largest": float64(3),
|
||||
"index_completed": float64(6),
|
||||
"suggest_threads": float64(2),
|
||||
"suggest_queue": float64(7),
|
||||
"suggest_active": float64(2),
|
||||
"suggest_rejected": float64(1),
|
||||
"suggest_largest": float64(8),
|
||||
"suggest_completed": float64(3),
|
||||
"fetch_shard_store_queue": float64(7),
|
||||
"fetch_shard_store_active": float64(4),
|
||||
"fetch_shard_store_rejected": float64(2),
|
||||
"fetch_shard_store_largest": float64(4),
|
||||
"fetch_shard_store_completed": float64(1),
|
||||
"fetch_shard_store_threads": float64(1),
|
||||
"management_threads": float64(2),
|
||||
"management_queue": float64(3),
|
||||
"management_active": float64(1),
|
||||
"management_rejected": float64(6),
|
||||
"management_largest": float64(2),
|
||||
"management_completed": float64(22),
|
||||
"percolate_queue": float64(23),
|
||||
"percolate_active": float64(13),
|
||||
"percolate_rejected": float64(235),
|
||||
"percolate_largest": float64(23),
|
||||
"percolate_completed": float64(33),
|
||||
"percolate_threads": float64(123),
|
||||
"listener_active": float64(4),
|
||||
"listener_rejected": float64(8),
|
||||
"listener_largest": float64(1),
|
||||
"listener_completed": float64(1),
|
||||
"listener_threads": float64(1),
|
||||
"listener_queue": float64(2),
|
||||
"search_rejected": float64(7),
|
||||
"search_largest": float64(2),
|
||||
"search_completed": float64(4),
|
||||
"search_threads": float64(5),
|
||||
"search_queue": float64(7),
|
||||
"search_active": float64(2),
|
||||
"fetch_shard_started_threads": float64(3),
|
||||
"fetch_shard_started_queue": float64(1),
|
||||
"fetch_shard_started_active": float64(5),
|
||||
"fetch_shard_started_rejected": float64(6),
|
||||
"fetch_shard_started_largest": float64(4),
|
||||
"fetch_shard_started_completed": float64(54),
|
||||
"refresh_rejected": float64(4),
|
||||
"refresh_largest": float64(8),
|
||||
"refresh_completed": float64(3),
|
||||
"refresh_threads": float64(23),
|
||||
"refresh_queue": float64(7),
|
||||
"refresh_active": float64(3),
|
||||
"optimize_threads": float64(3),
|
||||
"optimize_queue": float64(4),
|
||||
"optimize_active": float64(1),
|
||||
"optimize_rejected": float64(2),
|
||||
"optimize_largest": float64(7),
|
||||
"optimize_completed": float64(3),
|
||||
"snapshot_largest": float64(1),
|
||||
"snapshot_completed": float64(0),
|
||||
"snapshot_threads": float64(8),
|
||||
"snapshot_queue": float64(5),
|
||||
"snapshot_active": float64(6),
|
||||
"snapshot_rejected": float64(2),
|
||||
"generic_threads": float64(1),
|
||||
"generic_queue": float64(4),
|
||||
"generic_active": float64(6),
|
||||
"generic_rejected": float64(3),
|
||||
"generic_largest": float64(2),
|
||||
"generic_completed": float64(27),
|
||||
"flush_threads": float64(3),
|
||||
"flush_queue": float64(8),
|
||||
"flush_active": float64(0),
|
||||
"flush_rejected": float64(1),
|
||||
"flush_largest": float64(5),
|
||||
"flush_completed": float64(3),
|
||||
}
|
||||
|
||||
var fsExpected = map[string]interface{}{
|
||||
"timestamp": float64(1436460392946),
|
||||
"total_free_in_bytes": float64(16909316096),
|
||||
"total_available_in_bytes": float64(15894814720),
|
||||
"total_total_in_bytes": float64(19507089408),
|
||||
}
|
||||
|
||||
var transportExpected = map[string]interface{}{
|
||||
"server_open": float64(13),
|
||||
"rx_count": float64(6),
|
||||
"rx_size_in_bytes": float64(1380),
|
||||
"tx_count": float64(6),
|
||||
"tx_size_in_bytes": float64(1380),
|
||||
}
|
||||
|
||||
var httpExpected = map[string]interface{}{
|
||||
"current_open": float64(3),
|
||||
"total_opened": float64(3),
|
||||
}
|
||||
|
||||
var breakersExpected = map[string]interface{}{
|
||||
"fielddata_estimated_size_in_bytes": float64(0),
|
||||
"fielddata_overhead": float64(1.03),
|
||||
"fielddata_tripped": float64(0),
|
||||
"fielddata_limit_size_in_bytes": float64(623326003),
|
||||
"request_estimated_size_in_bytes": float64(0),
|
||||
"request_overhead": float64(1.0),
|
||||
"request_tripped": float64(0),
|
||||
"request_limit_size_in_bytes": float64(415550668),
|
||||
"parent_overhead": float64(1.0),
|
||||
"parent_tripped": float64(0),
|
||||
"parent_limit_size_in_bytes": float64(727213670),
|
||||
"parent_estimated_size_in_bytes": float64(0),
|
||||
}
|
||||
42
plugins/inputs/exec/README.md
Normal file
42
plugins/inputs/exec/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Exec Plugin
|
||||
|
||||
The exec plugin can execute arbitrary commands which output JSON. Then it flattens JSON and finds
|
||||
all numeric values, treating them as floats.
|
||||
|
||||
For example, if you have a json-returning command called mycollector, you could
|
||||
setup the exec plugin with:
|
||||
|
||||
```
|
||||
[[exec.commands]]
|
||||
command = "/usr/bin/mycollector --output=json"
|
||||
name = "mycollector"
|
||||
interval = 10
|
||||
```
|
||||
|
||||
The name is used as a prefix for the measurements.
|
||||
|
||||
The interval is used to determine how often a particular command should be run. Each
|
||||
time the exec plugin runs, it will only run a particular command if it has been at least
|
||||
`interval` seconds since the exec plugin last ran the command.
|
||||
|
||||
|
||||
# Sample
|
||||
|
||||
Let's say that we have a command named "mycollector", which gives the following output:
|
||||
```json
|
||||
{
|
||||
"a": 0.5,
|
||||
"b": {
|
||||
"c": "some text",
|
||||
"d": 0.1,
|
||||
"e": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The collected metrics will be:
|
||||
```
|
||||
exec_mycollector_a value=0.5
|
||||
exec_mycollector_b_d value=0.1
|
||||
exec_mycollector_b_e value=5
|
||||
```
|
||||
91
plugins/inputs/exec/exec.go
Normal file
91
plugins/inputs/exec/exec.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gonuts/go-shellquote"
|
||||
|
||||
"github.com/influxdb/telegraf/internal"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
const sampleConfig = `
|
||||
# the command to run
|
||||
command = "/usr/bin/mycollector --foo=bar"
|
||||
|
||||
# measurement name suffix (for separating different commands)
|
||||
name_suffix = "_mycollector"
|
||||
`
|
||||
|
||||
type Exec struct {
|
||||
Command string
|
||||
|
||||
runner Runner
|
||||
}
|
||||
|
||||
type Runner interface {
|
||||
Run(*Exec) ([]byte, error)
|
||||
}
|
||||
|
||||
type CommandRunner struct{}
|
||||
|
||||
func (c CommandRunner) Run(e *Exec) ([]byte, error) {
|
||||
split_cmd, err := shellquote.Split(e.Command)
|
||||
if err != nil || len(split_cmd) == 0 {
|
||||
return nil, fmt.Errorf("exec: unable to parse command, %s", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(split_cmd[0], split_cmd[1:]...)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("exec: %s for command '%s'", err, e.Command)
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
func NewExec() *Exec {
|
||||
return &Exec{runner: CommandRunner{}}
|
||||
}
|
||||
|
||||
func (e *Exec) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (e *Exec) Description() string {
|
||||
return "Read flattened metrics from one or more commands that output JSON to stdout"
|
||||
}
|
||||
|
||||
func (e *Exec) Gather(acc inputs.Accumulator) error {
|
||||
out, err := e.runner.Run(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var jsonOut interface{}
|
||||
err = json.Unmarshal(out, &jsonOut)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec: unable to parse output of '%s' as JSON, %s",
|
||||
e.Command, err)
|
||||
}
|
||||
|
||||
f := internal.JSONFlattener{}
|
||||
err = f.FlattenJSON("", jsonOut)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
acc.AddFields("exec", f.Fields, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("exec", func() inputs.Input {
|
||||
return NewExec()
|
||||
})
|
||||
}
|
||||
95
plugins/inputs/exec/exec_test.go
Normal file
95
plugins/inputs/exec/exec_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Midnight 9/22/2015
|
||||
const baseTimeSeconds = 1442905200
|
||||
|
||||
const validJson = `
|
||||
{
|
||||
"status": "green",
|
||||
"num_processes": 82,
|
||||
"cpu": {
|
||||
"status": "red",
|
||||
"nil_status": null,
|
||||
"used": 8234,
|
||||
"free": 32
|
||||
},
|
||||
"percent": 0.81,
|
||||
"users": [0, 1, 2, 3]
|
||||
}`
|
||||
|
||||
const malformedJson = `
|
||||
{
|
||||
"status": "green",
|
||||
`
|
||||
|
||||
type runnerMock struct {
|
||||
out []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func newRunnerMock(out []byte, err error) Runner {
|
||||
return &runnerMock{
|
||||
out: out,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (r runnerMock) Run(e *Exec) ([]byte, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
return r.out, nil
|
||||
}
|
||||
|
||||
func TestExec(t *testing.T) {
|
||||
e := &Exec{
|
||||
runner: newRunnerMock([]byte(validJson), nil),
|
||||
Command: "testcommand arg1",
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := e.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, acc.NFields(), 4, "non-numeric measurements should be ignored")
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"num_processes": float64(82),
|
||||
"cpu_used": float64(8234),
|
||||
"cpu_free": float64(32),
|
||||
"percent": float64(0.81),
|
||||
}
|
||||
acc.AssertContainsFields(t, "exec", fields)
|
||||
}
|
||||
|
||||
func TestExecMalformed(t *testing.T) {
|
||||
e := &Exec{
|
||||
runner: newRunnerMock([]byte(malformedJson), nil),
|
||||
Command: "badcommand arg1",
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := e.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, acc.NFields(), 0, "No new points should have been added")
|
||||
}
|
||||
|
||||
func TestCommandError(t *testing.T) {
|
||||
e := &Exec{
|
||||
runner: newRunnerMock(nil, fmt.Errorf("exit status code 1")),
|
||||
Command: "badcommand",
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := e.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, acc.NFields(), 0, "No new points should have been added")
|
||||
}
|
||||
364
plugins/inputs/haproxy/haproxy.go
Normal file
364
plugins/inputs/haproxy/haproxy.go
Normal file
@@ -0,0 +1,364 @@
|
||||
package haproxy
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
//CSV format: https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#9.1
|
||||
const (
|
||||
HF_PXNAME = 0 // 0. pxname [LFBS]: proxy name
|
||||
HF_SVNAME = 1 // 1. svname [LFBS]: service name (FRONTEND for frontend, BACKEND for backend, any name for server/listener)
|
||||
HF_QCUR = 2 //2. qcur [..BS]: current queued requests. For the backend this reports the number queued without a server assigned.
|
||||
HF_QMAX = 3 //3. qmax [..BS]: max value of qcur
|
||||
HF_SCUR = 4 // 4. scur [LFBS]: current sessions
|
||||
HF_SMAX = 5 //5. smax [LFBS]: max sessions
|
||||
HF_SLIM = 6 //6. slim [LFBS]: configured session limit
|
||||
HF_STOT = 7 //7. stot [LFBS]: cumulative number of connections
|
||||
HF_BIN = 8 //8. bin [LFBS]: bytes in
|
||||
HF_BOUT = 9 //9. bout [LFBS]: bytes out
|
||||
HF_DREQ = 10 //10. dreq [LFB.]: requests denied because of security concerns.
|
||||
HF_DRESP = 11 //11. dresp [LFBS]: responses denied because of security concerns.
|
||||
HF_EREQ = 12 //12. ereq [LF..]: request errors. Some of the possible causes are:
|
||||
HF_ECON = 13 //13. econ [..BS]: number of requests that encountered an error trying to
|
||||
HF_ERESP = 14 //14. eresp [..BS]: response errors. srv_abrt will be counted here also. Some other errors are: - write error on the client socket (won't be counted for the server stat) - failure applying filters to the response.
|
||||
HF_WRETR = 15 //15. wretr [..BS]: number of times a connection to a server was retried.
|
||||
HF_WREDIS = 16 //16. wredis [..BS]: number of times a request was redispatched to another server. The server value counts the number of times that server was switched away from.
|
||||
HF_STATUS = 17 //17. status [LFBS]: status (UP/DOWN/NOLB/MAINT/MAINT(via)...)
|
||||
HF_WEIGHT = 18 //18. weight [..BS]: total weight (backend), server weight (server)
|
||||
HF_ACT = 19 //19. act [..BS]: number of active servers (backend), server is active (server)
|
||||
HF_BCK = 20 //20. bck [..BS]: number of backup servers (backend), server is backup (server)
|
||||
HF_CHKFAIL = 21 //21. chkfail [...S]: number of failed checks. (Only counts checks failed when the server is up.)
|
||||
HF_CHKDOWN = 22 //22. chkdown [..BS]: number of UP->DOWN transitions. The backend counter counts transitions to the whole backend being down, rather than the sum of the counters for each server.
|
||||
HF_LASTCHG = 23 //23. lastchg [..BS]: number of seconds since the last UP<->DOWN transition
|
||||
HF_DOWNTIME = 24 //24. downtime [..BS]: total downtime (in seconds). The value for the backend is the downtime for the whole backend, not the sum of the server downtime.
|
||||
HF_QLIMIT = 25 //25. qlimit [...S]: configured maxqueue for the server, or nothing in the value is 0 (default, meaning no limit)
|
||||
HF_PID = 26 //26. pid [LFBS]: process id (0 for first instance, 1 for second, ...)
|
||||
HF_IID = 27 //27. iid [LFBS]: unique proxy id
|
||||
HF_SID = 28 //28. sid [L..S]: server id (unique inside a proxy)
|
||||
HF_THROTTLE = 29 //29. throttle [...S]: current throttle percentage for the server, when slowstart is active, or no value if not in slowstart.
|
||||
HF_LBTOT = 30 //30. lbtot [..BS]: total number of times a server was selected, either for new sessions, or when re-dispatching. The server counter is the number of times that server was selected.
|
||||
HF_TRACKED = 31 //31. tracked [...S]: id of proxy/server if tracking is enabled.
|
||||
HF_TYPE = 32 //32. type [LFBS]: (0 = frontend, 1 = backend, 2 = server, 3 = socket/listener)
|
||||
HF_RATE = 33 //33. rate [.FBS]: number of sessions per second over last elapsed second
|
||||
HF_RATE_LIM = 34 //34. rate_lim [.F..]: configured limit on new sessions per second
|
||||
HF_RATE_MAX = 35 //35. rate_max [.FBS]: max number of new sessions per second
|
||||
HF_CHECK_STATUS = 36 //36. check_status [...S]: status of last health check, one of:
|
||||
HF_CHECK_CODE = 37 //37. check_code [...S]: layer5-7 code, if available
|
||||
HF_CHECK_DURATION = 38 //38. check_duration [...S]: time in ms took to finish last health check
|
||||
HF_HRSP_1xx = 39 //39. hrsp_1xx [.FBS]: http responses with 1xx code
|
||||
HF_HRSP_2xx = 40 //40. hrsp_2xx [.FBS]: http responses with 2xx code
|
||||
HF_HRSP_3xx = 41 //41. hrsp_3xx [.FBS]: http responses with 3xx code
|
||||
HF_HRSP_4xx = 42 //42. hrsp_4xx [.FBS]: http responses with 4xx code
|
||||
HF_HRSP_5xx = 43 //43. hrsp_5xx [.FBS]: http responses with 5xx code
|
||||
HF_HRSP_OTHER = 44 //44. hrsp_other [.FBS]: http responses with other codes (protocol error)
|
||||
HF_HANAFAIL = 45 //45. hanafail [...S]: failed health checks details
|
||||
HF_REQ_RATE = 46 //46. req_rate [.F..]: HTTP requests per second over last elapsed second
|
||||
HF_REQ_RATE_MAX = 47 //47. req_rate_max [.F..]: max number of HTTP requests per second observed
|
||||
HF_REQ_TOT = 48 //48. req_tot [.F..]: total number of HTTP requests received
|
||||
HF_CLI_ABRT = 49 //49. cli_abrt [..BS]: number of data transfers aborted by the client
|
||||
HF_SRV_ABRT = 50 //50. srv_abrt [..BS]: number of data transfers aborted by the server (inc. in eresp)
|
||||
HF_COMP_IN = 51 //51. comp_in [.FB.]: number of HTTP response bytes fed to the compressor
|
||||
HF_COMP_OUT = 52 //52. comp_out [.FB.]: number of HTTP response bytes emitted by the compressor
|
||||
HF_COMP_BYP = 53 //53. comp_byp [.FB.]: number of bytes that bypassed the HTTP compressor (CPU/BW limit)
|
||||
HF_COMP_RSP = 54 //54. comp_rsp [.FB.]: number of HTTP responses that were compressed
|
||||
HF_LASTSESS = 55 //55. lastsess [..BS]: number of seconds since last session assigned to server/backend
|
||||
HF_LAST_CHK = 56 //56. last_chk [...S]: last health check contents or textual error
|
||||
HF_LAST_AGT = 57 //57. last_agt [...S]: last agent check contents or textual error
|
||||
HF_QTIME = 58 //58. qtime [..BS]:
|
||||
HF_CTIME = 59 //59. ctime [..BS]:
|
||||
HF_RTIME = 60 //60. rtime [..BS]: (0 for TCP)
|
||||
HF_TTIME = 61 //61. ttime [..BS]: the average total session time in ms over the 1024 last requests
|
||||
)
|
||||
|
||||
type haproxy struct {
|
||||
Servers []string
|
||||
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of address to gather stats about. Specify an ip on hostname
|
||||
# with optional port. ie localhost, 10.10.3.33:1936, etc.
|
||||
#
|
||||
# If no servers are specified, then default to 127.0.0.1:1936
|
||||
servers = ["http://myhaproxy.com:1936", "http://anotherhaproxy.com:1936"]
|
||||
# Or you can also use local socket(not work yet)
|
||||
# servers = ["socket://run/haproxy/admin.sock"]
|
||||
`
|
||||
|
||||
func (r *haproxy) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (r *haproxy) Description() string {
|
||||
return "Read metrics of haproxy, via socket or csv stats page"
|
||||
}
|
||||
|
||||
// Reads stats from all configured servers accumulates stats.
|
||||
// Returns one of the errors encountered while gather stats (if any).
|
||||
func (g *haproxy) Gather(acc inputs.Accumulator) error {
|
||||
if len(g.Servers) == 0 {
|
||||
return g.gatherServer("http://127.0.0.1:1936", acc)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var outerr error
|
||||
|
||||
for _, serv := range g.Servers {
|
||||
wg.Add(1)
|
||||
go func(serv string) {
|
||||
defer wg.Done()
|
||||
outerr = g.gatherServer(serv, acc)
|
||||
}(serv)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
func (g *haproxy) gatherServer(addr string, acc inputs.Accumulator) error {
|
||||
if g.client == nil {
|
||||
|
||||
client := &http.Client{}
|
||||
g.client = client
|
||||
}
|
||||
|
||||
u, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable parse server address '%s': %s", addr, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s://%s%s/;csv", u.Scheme, u.Host, u.Path), nil)
|
||||
if u.User != nil {
|
||||
p, _ := u.User.Password()
|
||||
req.SetBasicAuth(u.User.Username(), p)
|
||||
}
|
||||
|
||||
res, err := g.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to connect to haproxy server '%s': %s", addr, err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("Unable to get valid stat result from '%s': %s", addr, err)
|
||||
}
|
||||
|
||||
return importCsvResult(res.Body, acc, u.Host)
|
||||
}
|
||||
|
||||
func importCsvResult(r io.Reader, acc inputs.Accumulator, host string) error {
|
||||
csv := csv.NewReader(r)
|
||||
result, err := csv.ReadAll()
|
||||
now := time.Now()
|
||||
|
||||
for _, row := range result {
|
||||
fields := make(map[string]interface{})
|
||||
tags := map[string]string{
|
||||
"server": host,
|
||||
"proxy": row[HF_PXNAME],
|
||||
"sv": row[HF_SVNAME],
|
||||
}
|
||||
for field, v := range row {
|
||||
switch field {
|
||||
case HF_QCUR:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["qcur"] = ival
|
||||
}
|
||||
case HF_QMAX:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["qmax"] = ival
|
||||
}
|
||||
case HF_SCUR:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["scur"] = ival
|
||||
}
|
||||
case HF_SMAX:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["smax"] = ival
|
||||
}
|
||||
case HF_STOT:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["stot"] = ival
|
||||
}
|
||||
case HF_BIN:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["bin"] = ival
|
||||
}
|
||||
case HF_BOUT:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["bout"] = ival
|
||||
}
|
||||
case HF_DREQ:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["dreq"] = ival
|
||||
}
|
||||
case HF_DRESP:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["dresp"] = ival
|
||||
}
|
||||
case HF_EREQ:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["ereq"] = ival
|
||||
}
|
||||
case HF_ECON:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["econ"] = ival
|
||||
}
|
||||
case HF_ERESP:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["eresp"] = ival
|
||||
}
|
||||
case HF_WRETR:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["wretr"] = ival
|
||||
}
|
||||
case HF_WREDIS:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["wredis"] = ival
|
||||
}
|
||||
case HF_ACT:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["active_servers"] = ival
|
||||
}
|
||||
case HF_BCK:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["backup_servers"] = ival
|
||||
}
|
||||
case HF_DOWNTIME:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["downtime"] = ival
|
||||
}
|
||||
case HF_THROTTLE:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["throttle"] = ival
|
||||
}
|
||||
case HF_LBTOT:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["lbtot"] = ival
|
||||
}
|
||||
case HF_RATE:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["rate"] = ival
|
||||
}
|
||||
case HF_RATE_MAX:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["rate_max"] = ival
|
||||
}
|
||||
case HF_CHECK_DURATION:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["check_duration"] = ival
|
||||
}
|
||||
case HF_HRSP_1xx:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["http_response.1xx"] = ival
|
||||
}
|
||||
case HF_HRSP_2xx:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["http_response.2xx"] = ival
|
||||
}
|
||||
case HF_HRSP_3xx:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["http_response.3xx"] = ival
|
||||
}
|
||||
case HF_HRSP_4xx:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["http_response.4xx"] = ival
|
||||
}
|
||||
case HF_HRSP_5xx:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["http_response.5xx"] = ival
|
||||
}
|
||||
case HF_REQ_RATE:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["req_rate"] = ival
|
||||
}
|
||||
case HF_REQ_RATE_MAX:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["req_rate_max"] = ival
|
||||
}
|
||||
case HF_REQ_TOT:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["req_tot"] = ival
|
||||
}
|
||||
case HF_CLI_ABRT:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["cli_abort"] = ival
|
||||
}
|
||||
case HF_SRV_ABRT:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["srv_abort"] = ival
|
||||
}
|
||||
case HF_QTIME:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["qtime"] = ival
|
||||
}
|
||||
case HF_CTIME:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["ctime"] = ival
|
||||
}
|
||||
case HF_RTIME:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["rtime"] = ival
|
||||
}
|
||||
case HF_TTIME:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["ttime"] = ival
|
||||
}
|
||||
}
|
||||
}
|
||||
acc.AddFields("haproxy", fields, tags, now)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("haproxy", func() inputs.Input {
|
||||
return &haproxy{}
|
||||
})
|
||||
}
|
||||
179
plugins/inputs/haproxy/haproxy_test.go
Normal file
179
plugins/inputs/haproxy/haproxy_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package haproxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
func TestHaproxyGeneratesMetricsWithAuthentication(t *testing.T) {
|
||||
//We create a fake server to return test data
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
if username == "user" && password == "password" {
|
||||
fmt.Fprint(w, csvOutputSample)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
//Now we tested again above server, with our authentication data
|
||||
r := &haproxy{
|
||||
Servers: []string{strings.Replace(ts.URL, "http://", "http://user:password@", 1)},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := r.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags := map[string]string{
|
||||
"server": ts.Listener.Addr().String(),
|
||||
"proxy": "be_app",
|
||||
"sv": "host0",
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"active_servers": uint64(1),
|
||||
"backup_servers": uint64(0),
|
||||
"bin": uint64(510913516),
|
||||
"bout": uint64(2193856571),
|
||||
"check_duration": uint64(10),
|
||||
"cli_abort": uint64(73),
|
||||
"ctime": uint64(2),
|
||||
"downtime": uint64(0),
|
||||
"dresp": uint64(0),
|
||||
"econ": uint64(0),
|
||||
"eresp": uint64(1),
|
||||
"http_response.1xx": uint64(0),
|
||||
"http_response.2xx": uint64(119534),
|
||||
"http_response.3xx": uint64(48051),
|
||||
"http_response.4xx": uint64(2345),
|
||||
"http_response.5xx": uint64(1056),
|
||||
"lbtot": uint64(171013),
|
||||
"qcur": uint64(0),
|
||||
"qmax": uint64(0),
|
||||
"qtime": uint64(0),
|
||||
"rate": uint64(3),
|
||||
"rate_max": uint64(12),
|
||||
"rtime": uint64(312),
|
||||
"scur": uint64(1),
|
||||
"smax": uint64(32),
|
||||
"srv_abort": uint64(1),
|
||||
"stot": uint64(171014),
|
||||
"ttime": uint64(2341),
|
||||
"wredis": uint64(0),
|
||||
"wretr": uint64(1),
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "haproxy", fields, tags)
|
||||
|
||||
//Here, we should get error because we don't pass authentication data
|
||||
r = &haproxy{
|
||||
Servers: []string{ts.URL},
|
||||
}
|
||||
|
||||
err = r.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHaproxyGeneratesMetricsWithoutAuthentication(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, csvOutputSample)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
r := &haproxy{
|
||||
Servers: []string{ts.URL},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := r.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags := map[string]string{
|
||||
"proxy": "be_app",
|
||||
"server": ts.Listener.Addr().String(),
|
||||
"sv": "host0",
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"active_servers": uint64(1),
|
||||
"backup_servers": uint64(0),
|
||||
"bin": uint64(510913516),
|
||||
"bout": uint64(2193856571),
|
||||
"check_duration": uint64(10),
|
||||
"cli_abort": uint64(73),
|
||||
"ctime": uint64(2),
|
||||
"downtime": uint64(0),
|
||||
"dresp": uint64(0),
|
||||
"econ": uint64(0),
|
||||
"eresp": uint64(1),
|
||||
"http_response.1xx": uint64(0),
|
||||
"http_response.2xx": uint64(119534),
|
||||
"http_response.3xx": uint64(48051),
|
||||
"http_response.4xx": uint64(2345),
|
||||
"http_response.5xx": uint64(1056),
|
||||
"lbtot": uint64(171013),
|
||||
"qcur": uint64(0),
|
||||
"qmax": uint64(0),
|
||||
"qtime": uint64(0),
|
||||
"rate": uint64(3),
|
||||
"rate_max": uint64(12),
|
||||
"rtime": uint64(312),
|
||||
"scur": uint64(1),
|
||||
"smax": uint64(32),
|
||||
"srv_abort": uint64(1),
|
||||
"stot": uint64(171014),
|
||||
"ttime": uint64(2341),
|
||||
"wredis": uint64(0),
|
||||
"wretr": uint64(1),
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "haproxy", fields, tags)
|
||||
}
|
||||
|
||||
//When not passing server config, we default to localhost
|
||||
//We just want to make sure we did request stat from localhost
|
||||
func TestHaproxyDefaultGetFromLocalhost(t *testing.T) {
|
||||
r := &haproxy{}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := r.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "127.0.0.1:1936/;csv")
|
||||
}
|
||||
|
||||
const csvOutputSample = `
|
||||
# pxname,svname,qcur,qmax,scur,smax,slim,stot,bin,bout,dreq,dresp,ereq,econ,eresp,wretr,wredis,status,weight,act,bck,chkfail,chkdown,lastchg,downtime,qlimit,pid,iid,sid,throttle,lbtot,tracked,type,rate,rate_lim,rate_max,check_status,check_code,check_duration,hrsp_1xx,hrsp_2xx,hrsp_3xx,hrsp_4xx,hrsp_5xx,hrsp_other,hanafail,req_rate,req_rate_max,req_tot,cli_abrt,srv_abrt,comp_in,comp_out,comp_byp,comp_rsp,lastsess,last_chk,last_agt,qtime,ctime,rtime,ttime,
|
||||
fe_app,FRONTEND,,81,288,713,2000,1094063,5557055817,24096715169,1102,80,95740,,,17,19,OPEN,,,,,,,,,2,16,113,13,114,,0,18,0,102,,,,0,1314093,537036,123452,11966,1360,,35,140,1987928,,,0,0,0,0,,,,,,,,
|
||||
be_static,host0,0,0,0,3,,3209,1141294,17389596,,0,,0,0,0,0,no check,1,1,0,,,,,,2,17,1,,3209,,2,0,,7,,,,0,218,1497,1494,0,0,0,,,,0,0,,,,,2,,,0,2,23,545,
|
||||
be_static,BACKEND,0,0,0,3,200,3209,1141294,17389596,0,0,,0,0,0,0,UP,1,1,0,,0,70698,0,,2,17,0,,3209,,1,0,,7,,,,0,218,1497,1494,0,0,,,,,0,0,0,0,0,0,2,,,0,2,23,545,
|
||||
be_static,host0,0,0,0,1,,28,17313,466003,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,1,,28,,2,0,,1,L4OK,,1,0,17,6,5,0,0,0,,,,0,0,,,,,2103,,,0,1,1,36,
|
||||
be_static,host4,0,0,0,1,,28,15358,1281073,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,2,,28,,2,0,,1,L4OK,,1,0,20,5,3,0,0,0,,,,0,0,,,,,2076,,,0,1,1,54,
|
||||
be_static,host5,0,0,0,1,,28,17547,1970404,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,3,,28,,2,0,,1,L4OK,,0,0,20,5,3,0,0,0,,,,0,0,,,,,1495,,,0,1,1,53,
|
||||
be_static,host6,0,0,0,1,,28,14105,1328679,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,4,,28,,2,0,,1,L4OK,,0,0,18,8,2,0,0,0,,,,0,0,,,,,1418,,,0,0,1,49,
|
||||
be_static,host7,0,0,0,1,,28,15258,1965185,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,5,,28,,2,0,,1,L4OK,,0,0,17,8,3,0,0,0,,,,0,0,,,,,935,,,0,0,1,28,
|
||||
be_static,host8,0,0,0,1,,28,12934,1034779,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,6,,28,,2,0,,1,L4OK,,0,0,17,9,2,0,0,0,,,,0,0,,,,,582,,,0,1,1,66,
|
||||
be_static,host9,0,0,0,1,,28,13434,134063,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,7,,28,,2,0,,1,L4OK,,0,0,17,8,3,0,0,0,,,,0,0,,,,,539,,,0,0,1,80,
|
||||
be_static,host1,0,0,0,1,,28,7873,1209688,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,8,,28,,2,0,,1,L4OK,,0,0,22,6,0,0,0,0,,,,0,0,,,,,487,,,0,0,1,36,
|
||||
be_static,host2,0,0,0,1,,28,13830,1085929,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,9,,28,,2,0,,1,L4OK,,0,0,19,6,3,0,0,0,,,,0,0,,,,,338,,,0,1,1,38,
|
||||
be_static,host3,0,0,0,1,,28,17959,1259760,,0,,0,0,0,0,UP,1,1,0,0,0,70698,0,,2,18,10,,28,,2,0,,1,L4OK,,1,0,20,6,2,0,0,0,,,,0,0,,,,,92,,,0,1,1,17,
|
||||
be_static,BACKEND,0,0,0,2,200,307,160276,13322728,0,0,,0,0,0,0,UP,11,11,0,,0,70698,0,,2,18,0,,307,,1,0,,4,,,,0,205,73,29,0,0,,,,,0,0,0,0,0,0,92,,,0,1,3,381,
|
||||
be_app,host0,0,0,1,32,,171014,510913516,2193856571,,0,,0,1,1,0,UP,100,1,0,1,0,70698,0,,2,19,1,,171013,,2,3,,12,L7OK,301,10,0,119534,48051,2345,1056,0,0,,,,73,1,,,,,0,Moved Permanently,,0,2,312,2341,
|
||||
be_app,host4,0,0,2,29,,171013,499318742,2195595896,12,34,,0,2,0,0,UP,100,1,0,2,0,70698,0,,2,19,2,,171013,,2,3,,12,L7OK,301,12,0,119572,47882,2441,1088,0,0,,,,84,2,,,,,0,Moved Permanently,,0,2,316,2355,
|
||||
`
|
||||
148
plugins/inputs/httpjson/README.md
Normal file
148
plugins/inputs/httpjson/README.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# HTTP JSON Plugin
|
||||
|
||||
The httpjson plugin can collect data from remote URLs which respond with JSON. Then it flattens JSON and finds all numeric values, treating them as floats.
|
||||
|
||||
For example, if you have a service called _mycollector_, which has HTTP endpoint for gathering stats at http://my.service.com/_stats, you would configure the HTTP JSON
|
||||
plugin like this:
|
||||
|
||||
```
|
||||
[[httpjson.services]]
|
||||
name = "mycollector"
|
||||
|
||||
servers = [
|
||||
"http://my.service.com/_stats"
|
||||
]
|
||||
|
||||
# HTTP method to use (case-sensitive)
|
||||
method = "GET"
|
||||
```
|
||||
|
||||
`name` is used as a prefix for the measurements.
|
||||
|
||||
`method` specifies HTTP method to use for requests.
|
||||
|
||||
You can also specify which keys from server response should be considered tags:
|
||||
|
||||
```
|
||||
[[httpjson.services]]
|
||||
...
|
||||
|
||||
tag_keys = [
|
||||
"role",
|
||||
"version"
|
||||
]
|
||||
```
|
||||
|
||||
You can also specify additional request parameters for the service:
|
||||
|
||||
```
|
||||
[[httpjson.services]]
|
||||
...
|
||||
|
||||
[httpjson.services.parameters]
|
||||
event_type = "cpu_spike"
|
||||
threshold = "0.75"
|
||||
|
||||
```
|
||||
|
||||
|
||||
# Example:
|
||||
|
||||
Let's say that we have a service named "mycollector" configured like this:
|
||||
|
||||
```
|
||||
[httpjson]
|
||||
[[httpjson.services]]
|
||||
name = "mycollector"
|
||||
|
||||
servers = [
|
||||
"http://my.service.com/_stats"
|
||||
]
|
||||
|
||||
# HTTP method to use (case-sensitive)
|
||||
method = "GET"
|
||||
|
||||
tag_keys = ["service"]
|
||||
```
|
||||
|
||||
which responds with the following JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"service": "service01",
|
||||
"a": 0.5,
|
||||
"b": {
|
||||
"c": "some text",
|
||||
"d": 0.1,
|
||||
"e": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The collected metrics will be:
|
||||
```
|
||||
httpjson_mycollector_a,service='service01',server='http://my.service.com/_stats' value=0.5
|
||||
httpjson_mycollector_b_d,service='service01',server='http://my.service.com/_stats' value=0.1
|
||||
httpjson_mycollector_b_e,service='service01',server='http://my.service.com/_stats' value=5
|
||||
```
|
||||
|
||||
# Example 2, Multiple Services:
|
||||
|
||||
There is also the option to collect JSON from multiple services, here is an
|
||||
example doing that.
|
||||
|
||||
```
|
||||
[httpjson]
|
||||
[[httpjson.services]]
|
||||
name = "mycollector1"
|
||||
|
||||
servers = [
|
||||
"http://my.service1.com/_stats"
|
||||
]
|
||||
|
||||
# HTTP method to use (case-sensitive)
|
||||
method = "GET"
|
||||
|
||||
[[httpjson.services]]
|
||||
name = "mycollector2"
|
||||
|
||||
servers = [
|
||||
"http://service.net/json/stats"
|
||||
]
|
||||
|
||||
# HTTP method to use (case-sensitive)
|
||||
method = "POST"
|
||||
```
|
||||
|
||||
The services respond with the following JSON:
|
||||
|
||||
mycollector1:
|
||||
```json
|
||||
{
|
||||
"a": 0.5,
|
||||
"b": {
|
||||
"c": "some text",
|
||||
"d": 0.1,
|
||||
"e": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
mycollector2:
|
||||
```json
|
||||
{
|
||||
"load": 100,
|
||||
"users": 1335
|
||||
}
|
||||
```
|
||||
|
||||
The collected metrics will be:
|
||||
|
||||
```
|
||||
httpjson_mycollector1_a,server='http://my.service.com/_stats' value=0.5
|
||||
httpjson_mycollector1_b_d,server='http://my.service.com/_stats' value=0.1
|
||||
httpjson_mycollector1_b_e,server='http://my.service.com/_stats' value=5
|
||||
|
||||
httpjson_mycollector2_load,server='http://service.net/json/stats' value=100
|
||||
httpjson_mycollector2_users,server='http://service.net/json/stats' value=1335
|
||||
```
|
||||
216
plugins/inputs/httpjson/httpjson.go
Normal file
216
plugins/inputs/httpjson/httpjson.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package httpjson
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdb/telegraf/internal"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type HttpJson struct {
|
||||
Name string
|
||||
Servers []string
|
||||
Method string
|
||||
TagKeys []string
|
||||
Parameters map[string]string
|
||||
client HTTPClient
|
||||
}
|
||||
|
||||
type HTTPClient interface {
|
||||
// Returns the result of an http request
|
||||
//
|
||||
// Parameters:
|
||||
// req: HTTP request object
|
||||
//
|
||||
// Returns:
|
||||
// http.Response: HTTP respons object
|
||||
// error : Any error that may have occurred
|
||||
MakeRequest(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type RealHTTPClient struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (c RealHTTPClient) MakeRequest(req *http.Request) (*http.Response, error) {
|
||||
return c.client.Do(req)
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# a name for the service being polled
|
||||
name = "webserver_stats"
|
||||
|
||||
# URL of each server in the service's cluster
|
||||
servers = [
|
||||
"http://localhost:9999/stats/",
|
||||
"http://localhost:9998/stats/",
|
||||
]
|
||||
|
||||
# HTTP method to use (case-sensitive)
|
||||
method = "GET"
|
||||
|
||||
# List of tag names to extract from top-level of JSON server response
|
||||
# tag_keys = [
|
||||
# "my_tag_1",
|
||||
# "my_tag_2"
|
||||
# ]
|
||||
|
||||
# HTTP parameters (all values must be strings)
|
||||
[inputs.httpjson.parameters]
|
||||
event_type = "cpu_spike"
|
||||
threshold = "0.75"
|
||||
`
|
||||
|
||||
func (h *HttpJson) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (h *HttpJson) Description() string {
|
||||
return "Read flattened metrics from one or more JSON HTTP endpoints"
|
||||
}
|
||||
|
||||
// Gathers data for all servers.
|
||||
func (h *HttpJson) Gather(acc inputs.Accumulator) error {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
errorChannel := make(chan error, len(h.Servers))
|
||||
|
||||
for _, server := range h.Servers {
|
||||
wg.Add(1)
|
||||
go func(server string) {
|
||||
defer wg.Done()
|
||||
if err := h.gatherServer(acc, server); err != nil {
|
||||
errorChannel <- err
|
||||
}
|
||||
}(server)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errorChannel)
|
||||
|
||||
// Get all errors and return them as one giant error
|
||||
errorStrings := []string{}
|
||||
for err := range errorChannel {
|
||||
errorStrings = append(errorStrings, err.Error())
|
||||
}
|
||||
|
||||
if len(errorStrings) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(strings.Join(errorStrings, "\n"))
|
||||
}
|
||||
|
||||
// Gathers data from a particular server
|
||||
// Parameters:
|
||||
// acc : The telegraf Accumulator to use
|
||||
// serverURL: endpoint to send request to
|
||||
// service : the service being queried
|
||||
//
|
||||
// Returns:
|
||||
// error: Any error that may have occurred
|
||||
func (h *HttpJson) gatherServer(
|
||||
acc inputs.Accumulator,
|
||||
serverURL string,
|
||||
) error {
|
||||
resp, err := h.sendRequest(serverURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var jsonOut map[string]interface{}
|
||||
if err = json.Unmarshal([]byte(resp), &jsonOut); err != nil {
|
||||
return errors.New("Error decoding JSON response")
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"server": serverURL,
|
||||
}
|
||||
|
||||
for _, tag := range h.TagKeys {
|
||||
switch v := jsonOut[tag].(type) {
|
||||
case string:
|
||||
tags[tag] = v
|
||||
}
|
||||
delete(jsonOut, tag)
|
||||
}
|
||||
|
||||
f := internal.JSONFlattener{}
|
||||
err = f.FlattenJSON("", jsonOut)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var msrmnt_name string
|
||||
if h.Name == "" {
|
||||
msrmnt_name = "httpjson"
|
||||
} else {
|
||||
msrmnt_name = "httpjson_" + h.Name
|
||||
}
|
||||
acc.AddFields(msrmnt_name, f.Fields, tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sends an HTTP request to the server using the HttpJson object's HTTPClient
|
||||
// Parameters:
|
||||
// serverURL: endpoint to send request to
|
||||
//
|
||||
// Returns:
|
||||
// string: body of the response
|
||||
// error : Any error that may have occurred
|
||||
func (h *HttpJson) sendRequest(serverURL string) (string, error) {
|
||||
// Prepare URL
|
||||
requestURL, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Invalid server URL \"%s\"", serverURL)
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
for k, v := range h.Parameters {
|
||||
params.Add(k, v)
|
||||
}
|
||||
requestURL.RawQuery = params.Encode()
|
||||
|
||||
// Create + send request
|
||||
req, err := http.NewRequest(h.Method, requestURL.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := h.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return string(body), err
|
||||
}
|
||||
|
||||
// Process response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf("Response from url \"%s\" has status code %d (%s), expected %d (%s)",
|
||||
requestURL.String(),
|
||||
resp.StatusCode,
|
||||
http.StatusText(resp.StatusCode),
|
||||
http.StatusOK,
|
||||
http.StatusText(http.StatusOK))
|
||||
return string(body), err
|
||||
}
|
||||
|
||||
return string(body), err
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("httpjson", func() inputs.Input {
|
||||
return &HttpJson{client: RealHTTPClient{client: &http.Client{}}}
|
||||
})
|
||||
}
|
||||
198
plugins/inputs/httpjson/httpjson_test.go
Normal file
198
plugins/inputs/httpjson/httpjson_test.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package httpjson
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const validJSON = `
|
||||
{
|
||||
"parent": {
|
||||
"child": 3,
|
||||
"ignored_child": "hi"
|
||||
},
|
||||
"ignored_null": null,
|
||||
"integer": 4,
|
||||
"ignored_list": [3, 4],
|
||||
"ignored_parent": {
|
||||
"another_ignored_list": [4],
|
||||
"another_ignored_null": null,
|
||||
"ignored_string": "hello, world!"
|
||||
}
|
||||
}`
|
||||
|
||||
const validJSONTags = `
|
||||
{
|
||||
"value": 15,
|
||||
"role": "master",
|
||||
"build": "123"
|
||||
}`
|
||||
|
||||
var expectedFields = map[string]interface{}{
|
||||
"parent_child": float64(3),
|
||||
"integer": float64(4),
|
||||
}
|
||||
|
||||
const invalidJSON = "I don't think this is JSON"
|
||||
|
||||
const empty = ""
|
||||
|
||||
type mockHTTPClient struct {
|
||||
responseBody string
|
||||
statusCode int
|
||||
}
|
||||
|
||||
// Mock implementation of MakeRequest. Usually returns an http.Response with
|
||||
// hard-coded responseBody and statusCode. However, if the request uses a
|
||||
// nonstandard method, it uses status code 405 (method not allowed)
|
||||
func (c mockHTTPClient) MakeRequest(req *http.Request) (*http.Response, error) {
|
||||
resp := http.Response{}
|
||||
resp.StatusCode = c.statusCode
|
||||
|
||||
// basic error checking on request method
|
||||
allowedMethods := []string{"GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"}
|
||||
methodValid := false
|
||||
for _, method := range allowedMethods {
|
||||
if req.Method == method {
|
||||
methodValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !methodValid {
|
||||
resp.StatusCode = 405 // Method not allowed
|
||||
}
|
||||
|
||||
resp.Body = ioutil.NopCloser(strings.NewReader(c.responseBody))
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Generates a pointer to an HttpJson object that uses a mock HTTP client.
|
||||
// Parameters:
|
||||
// response : Body of the response that the mock HTTP client should return
|
||||
// statusCode: HTTP status code the mock HTTP client should return
|
||||
//
|
||||
// Returns:
|
||||
// *HttpJson: Pointer to an HttpJson object that uses the generated mock HTTP client
|
||||
func genMockHttpJson(response string, statusCode int) []*HttpJson {
|
||||
return []*HttpJson{
|
||||
&HttpJson{
|
||||
client: mockHTTPClient{responseBody: response, statusCode: statusCode},
|
||||
Servers: []string{
|
||||
"http://server1.example.com/metrics/",
|
||||
"http://server2.example.com/metrics/",
|
||||
},
|
||||
Name: "my_webapp",
|
||||
Method: "GET",
|
||||
Parameters: map[string]string{
|
||||
"httpParam1": "12",
|
||||
"httpParam2": "the second parameter",
|
||||
},
|
||||
},
|
||||
&HttpJson{
|
||||
client: mockHTTPClient{responseBody: response, statusCode: statusCode},
|
||||
Servers: []string{
|
||||
"http://server3.example.com/metrics/",
|
||||
"http://server4.example.com/metrics/",
|
||||
},
|
||||
Name: "other_webapp",
|
||||
Method: "POST",
|
||||
Parameters: map[string]string{
|
||||
"httpParam1": "12",
|
||||
"httpParam2": "the second parameter",
|
||||
},
|
||||
TagKeys: []string{
|
||||
"role",
|
||||
"build",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the proper values are ignored or collected
|
||||
func TestHttpJson200(t *testing.T) {
|
||||
httpjson := genMockHttpJson(validJSON, 200)
|
||||
|
||||
for _, service := range httpjson {
|
||||
var acc testutil.Accumulator
|
||||
err := service.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 4, acc.NFields())
|
||||
for _, srv := range service.Servers {
|
||||
tags := map[string]string{"server": srv}
|
||||
mname := "httpjson_" + service.Name
|
||||
acc.AssertContainsTaggedFields(t, mname, expectedFields, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test response to HTTP 500
|
||||
func TestHttpJson500(t *testing.T) {
|
||||
httpjson := genMockHttpJson(validJSON, 500)
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := httpjson[0].Gather(&acc)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 0, acc.NFields())
|
||||
}
|
||||
|
||||
// Test response to HTTP 405
|
||||
func TestHttpJsonBadMethod(t *testing.T) {
|
||||
httpjson := genMockHttpJson(validJSON, 200)
|
||||
httpjson[0].Method = "NOT_A_REAL_METHOD"
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := httpjson[0].Gather(&acc)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 0, acc.NFields())
|
||||
}
|
||||
|
||||
// Test response to malformed JSON
|
||||
func TestHttpJsonBadJson(t *testing.T) {
|
||||
httpjson := genMockHttpJson(invalidJSON, 200)
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := httpjson[0].Gather(&acc)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 0, acc.NFields())
|
||||
}
|
||||
|
||||
// Test response to empty string as response objectgT
|
||||
func TestHttpJsonEmptyResponse(t *testing.T) {
|
||||
httpjson := genMockHttpJson(empty, 200)
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := httpjson[0].Gather(&acc)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 0, acc.NFields())
|
||||
}
|
||||
|
||||
// Test that the proper values are ignored or collected
|
||||
func TestHttpJson200Tags(t *testing.T) {
|
||||
httpjson := genMockHttpJson(validJSONTags, 200)
|
||||
|
||||
for _, service := range httpjson {
|
||||
if service.Name == "other_webapp" {
|
||||
var acc testutil.Accumulator
|
||||
err := service.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, acc.NFields())
|
||||
for _, srv := range service.Servers {
|
||||
tags := map[string]string{"server": srv, "role": "master", "build": "123"}
|
||||
fields := map[string]interface{}{"value": float64(15)}
|
||||
mname := "httpjson_" + service.Name
|
||||
acc.AssertContainsTaggedFields(t, mname, fields, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
plugins/inputs/influxdb/README.md
Normal file
72
plugins/inputs/influxdb/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# influxdb plugin
|
||||
|
||||
The influxdb plugin collects InfluxDB-formatted data from JSON endpoints.
|
||||
|
||||
With a configuration of:
|
||||
|
||||
```toml
|
||||
[[inputs.influxdb]]
|
||||
urls = [
|
||||
"http://127.0.0.1:8086/debug/vars",
|
||||
"http://192.168.2.1:8086/debug/vars"
|
||||
]
|
||||
```
|
||||
|
||||
And if 127.0.0.1 responds with this JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"k1": {
|
||||
"name": "fruit",
|
||||
"tags": {
|
||||
"kind": "apple"
|
||||
},
|
||||
"values": {
|
||||
"inventory": 371,
|
||||
"sold": 112
|
||||
}
|
||||
},
|
||||
"k2": {
|
||||
"name": "fruit",
|
||||
"tags": {
|
||||
"kind": "banana"
|
||||
},
|
||||
"values": {
|
||||
"inventory": 1000,
|
||||
"sold": 403
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And if 192.168.2.1 responds like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"k3": {
|
||||
"name": "transactions",
|
||||
"tags": {},
|
||||
"values": {
|
||||
"total": 100,
|
||||
"balance": 184.75
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then the collected metrics will be:
|
||||
|
||||
```
|
||||
influxdb_fruit,url='http://127.0.0.1:8086/debug/vars',kind='apple' inventory=371.0,sold=112.0
|
||||
influxdb_fruit,url='http://127.0.0.1:8086/debug/vars',kind='banana' inventory=1000.0,sold=403.0
|
||||
|
||||
influxdb_transactions,url='http://192.168.2.1:8086/debug/vars' total=100.0,balance=184.75
|
||||
```
|
||||
|
||||
There are two important details to note about the collected metrics:
|
||||
|
||||
1. Even though the values in JSON are being displayed as integers, the metrics are reported as floats.
|
||||
JSON encoders usually don't print the fractional part for round floats.
|
||||
Because you cannot change the type of an existing field in InfluxDB, we assume all numbers are floats.
|
||||
|
||||
2. The top-level keys' names (in the example above, `"k1"`, `"k2"`, and `"k3"`) are not considered when recording the metrics.
|
||||
146
plugins/inputs/influxdb/influxdb.go
Normal file
146
plugins/inputs/influxdb/influxdb.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package influxdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type InfluxDB struct {
|
||||
URLs []string `toml:"urls"`
|
||||
}
|
||||
|
||||
func (*InfluxDB) Description() string {
|
||||
return "Read InfluxDB-formatted JSON metrics from one or more HTTP endpoints"
|
||||
}
|
||||
|
||||
func (*InfluxDB) SampleConfig() string {
|
||||
return `
|
||||
# Works with InfluxDB debug endpoints out of the box,
|
||||
# but other services can use this format too.
|
||||
# See the influxdb plugin's README for more details.
|
||||
|
||||
# Multiple URLs from which to read InfluxDB-formatted JSON
|
||||
urls = [
|
||||
"http://localhost:8086/debug/vars"
|
||||
]
|
||||
`
|
||||
}
|
||||
|
||||
func (i *InfluxDB) Gather(acc inputs.Accumulator) error {
|
||||
errorChannel := make(chan error, len(i.URLs))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, u := range i.URLs {
|
||||
wg.Add(1)
|
||||
go func(url string) {
|
||||
defer wg.Done()
|
||||
if err := i.gatherURL(acc, url); err != nil {
|
||||
errorChannel <- fmt.Errorf("[url=%s]: %s", url, err)
|
||||
}
|
||||
}(u)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errorChannel)
|
||||
|
||||
// If there weren't any errors, we can return nil now.
|
||||
if len(errorChannel) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// There were errors, so join them all together as one big error.
|
||||
errorStrings := make([]string, 0, len(errorChannel))
|
||||
for err := range errorChannel {
|
||||
errorStrings = append(errorStrings, err.Error())
|
||||
}
|
||||
|
||||
return errors.New(strings.Join(errorStrings, "\n"))
|
||||
}
|
||||
|
||||
type point struct {
|
||||
Name string `json:"name"`
|
||||
Tags map[string]string `json:"tags"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
}
|
||||
|
||||
// Gathers data from a particular URL
|
||||
// Parameters:
|
||||
// acc : The telegraf Accumulator to use
|
||||
// url : endpoint to send request to
|
||||
//
|
||||
// Returns:
|
||||
// error: Any error that may have occurred
|
||||
func (i *InfluxDB) gatherURL(
|
||||
acc inputs.Accumulator,
|
||||
url string,
|
||||
) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// It would be nice to be able to decode into a map[string]point, but
|
||||
// we'll get a decoder error like:
|
||||
// `json: cannot unmarshal array into Go value of type influxdb.point`
|
||||
// if any of the values aren't objects.
|
||||
// To avoid that error, we decode by hand.
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
|
||||
// Parse beginning of object
|
||||
if t, err := dec.Token(); err != nil {
|
||||
return err
|
||||
} else if t != json.Delim('{') {
|
||||
return errors.New("document root must be a JSON object")
|
||||
}
|
||||
|
||||
// Loop through rest of object
|
||||
for {
|
||||
// Nothing left in this object, we're done
|
||||
if !dec.More() {
|
||||
break
|
||||
}
|
||||
|
||||
// Read in a string key. We don't do anything with the top-level keys, so it's discarded.
|
||||
_, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attempt to parse a whole object into a point.
|
||||
// It might be a non-object, like a string or array.
|
||||
// If we fail to decode it into a point, ignore it and move on.
|
||||
var p point
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the object was a point, but was not fully initialized, ignore it and move on.
|
||||
if p.Name == "" || p.Tags == nil || p.Values == nil || len(p.Values) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add a tag to indicate the source of the data.
|
||||
p.Tags["url"] = url
|
||||
|
||||
acc.AddFields(
|
||||
p.Name,
|
||||
p.Values,
|
||||
p.Tags,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("influxdb", func() inputs.Input {
|
||||
return &InfluxDB{}
|
||||
})
|
||||
}
|
||||
97
plugins/inputs/influxdb/influxdb_test.go
Normal file
97
plugins/inputs/influxdb/influxdb_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package influxdb_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs/influxdb"
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
js := `
|
||||
{
|
||||
"_1": {
|
||||
"name": "foo",
|
||||
"tags": {
|
||||
"id": "ex1"
|
||||
},
|
||||
"values": {
|
||||
"i": -1,
|
||||
"f": 0.5,
|
||||
"b": true,
|
||||
"s": "string"
|
||||
}
|
||||
},
|
||||
"ignored": {
|
||||
"willBeRecorded": false
|
||||
},
|
||||
"ignoredAndNested": {
|
||||
"hash": {
|
||||
"is": "nested"
|
||||
}
|
||||
},
|
||||
"array": [
|
||||
"makes parsing more difficult than necessary"
|
||||
],
|
||||
"string": "makes parsing more difficult than necessary",
|
||||
"_2": {
|
||||
"name": "bar",
|
||||
"tags": {
|
||||
"id": "ex2"
|
||||
},
|
||||
"values": {
|
||||
"x": "x"
|
||||
}
|
||||
},
|
||||
"pointWithoutFields_willNotBeIncluded": {
|
||||
"name": "asdf",
|
||||
"tags": {
|
||||
"id": "ex3"
|
||||
},
|
||||
"values": {}
|
||||
}
|
||||
}
|
||||
`
|
||||
fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/endpoint" {
|
||||
_, _ = w.Write([]byte(js))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer fakeServer.Close()
|
||||
|
||||
plugin := &influxdb.InfluxDB{
|
||||
URLs: []string{fakeServer.URL + "/endpoint"},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
require.NoError(t, plugin.Gather(&acc))
|
||||
|
||||
require.Len(t, acc.Points, 2)
|
||||
fields := map[string]interface{}{
|
||||
// JSON will truncate floats to integer representations.
|
||||
// Since there's no distinction in JSON, we can't assume it's an int.
|
||||
"i": -1.0,
|
||||
"f": 0.5,
|
||||
"b": true,
|
||||
"s": "string",
|
||||
}
|
||||
tags := map[string]string{
|
||||
"id": "ex1",
|
||||
"url": fakeServer.URL + "/endpoint",
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "foo", fields, tags)
|
||||
|
||||
fields = map[string]interface{}{
|
||||
"x": "x",
|
||||
}
|
||||
tags = map[string]string{
|
||||
"id": "ex2",
|
||||
"url": fakeServer.URL + "/endpoint",
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "bar", fields, tags)
|
||||
}
|
||||
51
plugins/inputs/jolokia/README.md
Normal file
51
plugins/inputs/jolokia/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Telegraf plugin: Jolokia
|
||||
|
||||
#### Plugin arguments:
|
||||
- **context** string: Context root used of jolokia url
|
||||
- **servers** []Server: List of servers
|
||||
+ **name** string: Server's logical name
|
||||
+ **host** string: Server's ip address or hostname
|
||||
+ **port** string: Server's listening port
|
||||
- **metrics** []Metric
|
||||
+ **name** string: Name of the measure
|
||||
+ **jmx** string: Jmx path that identifies mbeans attributes
|
||||
+ **pass** []string: Attributes to retain when collecting values
|
||||
+ **drop** []string: Attributes to drop when collecting values
|
||||
|
||||
#### Description
|
||||
|
||||
The Jolokia plugin collects JVM metrics exposed as MBean's attributes through jolokia REST endpoint. All metrics
|
||||
are collected for each server configured.
|
||||
|
||||
See: https://jolokia.org/
|
||||
|
||||
# Measurements:
|
||||
Jolokia plugin produces one measure for each metric configured, adding Server's `name`, `host` and `port` as tags.
|
||||
|
||||
Given a configuration like:
|
||||
|
||||
```ini
|
||||
[jolokia]
|
||||
|
||||
[[jolokia.servers]]
|
||||
name = "as-service-1"
|
||||
host = "127.0.0.1"
|
||||
port = "8080"
|
||||
|
||||
[[jolokia.servers]]
|
||||
name = "as-service-2"
|
||||
host = "127.0.0.1"
|
||||
port = "8180"
|
||||
|
||||
[[jolokia.metrics]]
|
||||
name = "heap_memory_usage"
|
||||
jmx = "/java.lang:type=Memory/HeapMemoryUsage"
|
||||
pass = ["used", "max"]
|
||||
```
|
||||
|
||||
The collected metrics will be:
|
||||
|
||||
```
|
||||
jolokia_heap_memory_usage name=as-service-1,host=127.0.0.1,port=8080 used=xxx,max=yyy
|
||||
jolokia_heap_memory_usage name=as-service-2,host=127.0.0.1,port=8180 used=vvv,max=zzz
|
||||
```
|
||||
163
plugins/inputs/jolokia/jolokia.go
Normal file
163
plugins/inputs/jolokia/jolokia.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package jolokia
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Name string
|
||||
Host string
|
||||
Username string
|
||||
Password string
|
||||
Port string
|
||||
}
|
||||
|
||||
type Metric struct {
|
||||
Name string
|
||||
Jmx string
|
||||
}
|
||||
|
||||
type JolokiaClient interface {
|
||||
MakeRequest(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type JolokiaClientImpl struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (c JolokiaClientImpl) MakeRequest(req *http.Request) (*http.Response, error) {
|
||||
return c.client.Do(req)
|
||||
}
|
||||
|
||||
type Jolokia struct {
|
||||
jClient JolokiaClient
|
||||
Context string
|
||||
Servers []Server
|
||||
Metrics []Metric
|
||||
}
|
||||
|
||||
func (j *Jolokia) SampleConfig() string {
|
||||
return `
|
||||
# This is the context root used to compose the jolokia url
|
||||
context = "/jolokia/read"
|
||||
|
||||
# List of servers exposing jolokia read service
|
||||
[[inputs.jolokia.servers]]
|
||||
name = "stable"
|
||||
host = "192.168.103.2"
|
||||
port = "8180"
|
||||
# username = "myuser"
|
||||
# password = "mypassword"
|
||||
|
||||
# List of metrics collected on above servers
|
||||
# Each metric consists in a name, a jmx path and either a pass or drop slice attributes
|
||||
# This collect all heap memory usage metrics
|
||||
[[inputs.jolokia.metrics]]
|
||||
name = "heap_memory_usage"
|
||||
jmx = "/java.lang:type=Memory/HeapMemoryUsage"
|
||||
`
|
||||
}
|
||||
|
||||
func (j *Jolokia) Description() string {
|
||||
return "Read JMX metrics through Jolokia"
|
||||
}
|
||||
|
||||
func (j *Jolokia) getAttr(requestUrl *url.URL) (map[string]interface{}, error) {
|
||||
// Create + send request
|
||||
req, err := http.NewRequest("GET", requestUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := j.jClient.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Process response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf("Response from url \"%s\" has status code %d (%s), expected %d (%s)",
|
||||
requestUrl,
|
||||
resp.StatusCode,
|
||||
http.StatusText(resp.StatusCode),
|
||||
http.StatusOK,
|
||||
http.StatusText(http.StatusOK))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read body
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal json
|
||||
var jsonOut map[string]interface{}
|
||||
if err = json.Unmarshal([]byte(body), &jsonOut); err != nil {
|
||||
return nil, errors.New("Error decoding JSON response")
|
||||
}
|
||||
|
||||
return jsonOut, nil
|
||||
}
|
||||
|
||||
func (j *Jolokia) Gather(acc inputs.Accumulator) error {
|
||||
context := j.Context //"/jolokia/read"
|
||||
servers := j.Servers
|
||||
metrics := j.Metrics
|
||||
tags := make(map[string]string)
|
||||
|
||||
for _, server := range servers {
|
||||
tags["server"] = server.Name
|
||||
tags["port"] = server.Port
|
||||
tags["host"] = server.Host
|
||||
fields := make(map[string]interface{})
|
||||
for _, metric := range metrics {
|
||||
|
||||
measurement := metric.Name
|
||||
jmxPath := metric.Jmx
|
||||
|
||||
// Prepare URL
|
||||
requestUrl, err := url.Parse("http://" + server.Host + ":" +
|
||||
server.Port + context + jmxPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if server.Username != "" || server.Password != "" {
|
||||
requestUrl.User = url.UserPassword(server.Username, server.Password)
|
||||
}
|
||||
|
||||
out, _ := j.getAttr(requestUrl)
|
||||
|
||||
if values, ok := out["value"]; ok {
|
||||
switch t := values.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range t {
|
||||
fields[measurement+"_"+k] = v
|
||||
}
|
||||
case interface{}:
|
||||
fields[measurement] = t
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Missing key 'value' in '%s' output response\n",
|
||||
requestUrl.String())
|
||||
}
|
||||
}
|
||||
acc.AddFields("jolokia", fields, tags)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("jolokia", func() inputs.Input {
|
||||
return &Jolokia{jClient: &JolokiaClientImpl{client: &http.Client{}}}
|
||||
})
|
||||
}
|
||||
116
plugins/inputs/jolokia/jolokia_test.go
Normal file
116
plugins/inputs/jolokia/jolokia_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package jolokia
|
||||
|
||||
import (
|
||||
_ "fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const validMultiValueJSON = `
|
||||
{
|
||||
"request":{
|
||||
"mbean":"java.lang:type=Memory",
|
||||
"attribute":"HeapMemoryUsage",
|
||||
"type":"read"
|
||||
},
|
||||
"value":{
|
||||
"init":67108864,
|
||||
"committed":456130560,
|
||||
"max":477626368,
|
||||
"used":203288528
|
||||
},
|
||||
"timestamp":1446129191,
|
||||
"status":200
|
||||
}`
|
||||
|
||||
const validSingleValueJSON = `
|
||||
{
|
||||
"request":{
|
||||
"path":"used",
|
||||
"mbean":"java.lang:type=Memory",
|
||||
"attribute":"HeapMemoryUsage",
|
||||
"type":"read"
|
||||
},
|
||||
"value":209274376,
|
||||
"timestamp":1446129256,
|
||||
"status":200
|
||||
}`
|
||||
|
||||
const invalidJSON = "I don't think this is JSON"
|
||||
|
||||
const empty = ""
|
||||
|
||||
var Servers = []Server{Server{Name: "as1", Host: "127.0.0.1", Port: "8080"}}
|
||||
var HeapMetric = Metric{Name: "heap_memory_usage", Jmx: "/java.lang:type=Memory/HeapMemoryUsage"}
|
||||
var UsedHeapMetric = Metric{Name: "heap_memory_usage", Jmx: "/java.lang:type=Memory/HeapMemoryUsage"}
|
||||
|
||||
type jolokiaClientStub struct {
|
||||
responseBody string
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (c jolokiaClientStub) MakeRequest(req *http.Request) (*http.Response, error) {
|
||||
resp := http.Response{}
|
||||
resp.StatusCode = c.statusCode
|
||||
resp.Body = ioutil.NopCloser(strings.NewReader(c.responseBody))
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Generates a pointer to an HttpJson object that uses a mock HTTP client.
|
||||
// Parameters:
|
||||
// response : Body of the response that the mock HTTP client should return
|
||||
// statusCode: HTTP status code the mock HTTP client should return
|
||||
//
|
||||
// Returns:
|
||||
// *HttpJson: Pointer to an HttpJson object that uses the generated mock HTTP client
|
||||
func genJolokiaClientStub(response string, statusCode int, servers []Server, metrics []Metric) *Jolokia {
|
||||
return &Jolokia{
|
||||
jClient: jolokiaClientStub{responseBody: response, statusCode: statusCode},
|
||||
Servers: servers,
|
||||
Metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the proper values are ignored or collected
|
||||
func TestHttpJsonMultiValue(t *testing.T) {
|
||||
jolokia := genJolokiaClientStub(validMultiValueJSON, 200, Servers, []Metric{HeapMetric})
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := jolokia.Gather(&acc)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(acc.Points))
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"heap_memory_usage_init": 67108864.0,
|
||||
"heap_memory_usage_committed": 456130560.0,
|
||||
"heap_memory_usage_max": 477626368.0,
|
||||
"heap_memory_usage_used": 203288528.0,
|
||||
}
|
||||
tags := map[string]string{
|
||||
"host": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"server": "as1",
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "jolokia", fields, tags)
|
||||
}
|
||||
|
||||
// Test that the proper values are ignored or collected
|
||||
func TestHttpJsonOn404(t *testing.T) {
|
||||
|
||||
jolokia := genJolokiaClientStub(validMultiValueJSON, 404, Servers,
|
||||
[]Metric{UsedHeapMetric})
|
||||
|
||||
var acc testutil.Accumulator
|
||||
acc.SetDebug(true)
|
||||
err := jolokia.Gather(&acc)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(acc.Points))
|
||||
}
|
||||
24
plugins/inputs/kafka_consumer/README.md
Normal file
24
plugins/inputs/kafka_consumer/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Kafka Consumer
|
||||
|
||||
The [Kafka](http://kafka.apache.org/) consumer plugin polls a specified Kafka
|
||||
topic and adds messages to InfluxDB. The plugin assumes messages follow the
|
||||
line protocol. [Consumer Group](http://godoc.org/github.com/wvanbergen/kafka/consumergroup)
|
||||
is used to talk to the Kafka cluster so multiple instances of telegraf can read
|
||||
from the same topic in parallel.
|
||||
|
||||
## Testing
|
||||
|
||||
Running integration tests requires running Zookeeper & Kafka. The following
|
||||
commands assume you're on OS X & using [boot2docker](http://boot2docker.io/) or docker-machine through [Docker Toolbox](https://www.docker.com/docker-toolbox).
|
||||
|
||||
To start Kafka & Zookeeper:
|
||||
|
||||
```
|
||||
docker run -d -p 2181:2181 -p 9092:9092 --env ADVERTISED_HOST=`boot2docker ip || docker-machine ip <your_machine_name>` --env ADVERTISED_PORT=9092 spotify/kafka
|
||||
```
|
||||
|
||||
To run tests:
|
||||
|
||||
```
|
||||
go test
|
||||
```
|
||||
166
plugins/inputs/kafka_consumer/kafka_consumer.go
Normal file
166
plugins/inputs/kafka_consumer/kafka_consumer.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package kafka_consumer
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdb/influxdb/models"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/wvanbergen/kafka/consumergroup"
|
||||
)
|
||||
|
||||
type Kafka struct {
|
||||
ConsumerGroup string
|
||||
Topics []string
|
||||
ZookeeperPeers []string
|
||||
Consumer *consumergroup.ConsumerGroup
|
||||
PointBuffer int
|
||||
Offset string
|
||||
|
||||
sync.Mutex
|
||||
|
||||
// channel for all incoming kafka messages
|
||||
in <-chan *sarama.ConsumerMessage
|
||||
// channel for all kafka consumer errors
|
||||
errs <-chan *sarama.ConsumerError
|
||||
// channel for all incoming parsed kafka points
|
||||
pointChan chan models.Point
|
||||
done chan struct{}
|
||||
|
||||
// doNotCommitMsgs tells the parser not to call CommitUpTo on the consumer
|
||||
// this is mostly for test purposes, but there may be a use-case for it later.
|
||||
doNotCommitMsgs bool
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# topic(s) to consume
|
||||
topics = ["telegraf"]
|
||||
# an array of Zookeeper connection strings
|
||||
zookeeper_peers = ["localhost:2181"]
|
||||
# the name of the consumer group
|
||||
consumer_group = "telegraf_metrics_consumers"
|
||||
# Maximum number of points to buffer between collection intervals
|
||||
point_buffer = 100000
|
||||
# Offset (must be either "oldest" or "newest")
|
||||
offset = "oldest"
|
||||
`
|
||||
|
||||
func (k *Kafka) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (k *Kafka) Description() string {
|
||||
return "Read line-protocol metrics from Kafka topic(s)"
|
||||
}
|
||||
|
||||
func (k *Kafka) Start() error {
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
var consumerErr error
|
||||
|
||||
config := consumergroup.NewConfig()
|
||||
switch strings.ToLower(k.Offset) {
|
||||
case "oldest", "":
|
||||
config.Offsets.Initial = sarama.OffsetOldest
|
||||
case "newest":
|
||||
config.Offsets.Initial = sarama.OffsetNewest
|
||||
default:
|
||||
log.Printf("WARNING: Kafka consumer invalid offset '%s', using 'oldest'\n",
|
||||
k.Offset)
|
||||
config.Offsets.Initial = sarama.OffsetOldest
|
||||
}
|
||||
|
||||
if k.Consumer == nil || k.Consumer.Closed() {
|
||||
k.Consumer, consumerErr = consumergroup.JoinConsumerGroup(
|
||||
k.ConsumerGroup,
|
||||
k.Topics,
|
||||
k.ZookeeperPeers,
|
||||
config,
|
||||
)
|
||||
if consumerErr != nil {
|
||||
return consumerErr
|
||||
}
|
||||
|
||||
// Setup message and error channels
|
||||
k.in = k.Consumer.Messages()
|
||||
k.errs = k.Consumer.Errors()
|
||||
}
|
||||
|
||||
k.done = make(chan struct{})
|
||||
if k.PointBuffer == 0 {
|
||||
k.PointBuffer = 100000
|
||||
}
|
||||
k.pointChan = make(chan models.Point, k.PointBuffer)
|
||||
|
||||
// Start the kafka message reader
|
||||
go k.parser()
|
||||
log.Printf("Started the kafka consumer service, peers: %v, topics: %v\n",
|
||||
k.ZookeeperPeers, k.Topics)
|
||||
return nil
|
||||
}
|
||||
|
||||
// parser() reads all incoming messages from the consumer, and parses them into
|
||||
// influxdb metric points.
|
||||
func (k *Kafka) parser() {
|
||||
for {
|
||||
select {
|
||||
case <-k.done:
|
||||
return
|
||||
case err := <-k.errs:
|
||||
log.Printf("Kafka Consumer Error: %s\n", err.Error())
|
||||
case msg := <-k.in:
|
||||
points, err := models.ParsePoints(msg.Value)
|
||||
if err != nil {
|
||||
log.Printf("Could not parse kafka message: %s, error: %s",
|
||||
string(msg.Value), err.Error())
|
||||
}
|
||||
|
||||
for _, point := range points {
|
||||
select {
|
||||
case k.pointChan <- point:
|
||||
continue
|
||||
default:
|
||||
log.Printf("Kafka Consumer buffer is full, dropping a point." +
|
||||
" You may want to increase the point_buffer setting")
|
||||
}
|
||||
}
|
||||
|
||||
if !k.doNotCommitMsgs {
|
||||
// TODO(cam) this locking can be removed if this PR gets merged:
|
||||
// https://github.com/wvanbergen/kafka/pull/84
|
||||
k.Lock()
|
||||
k.Consumer.CommitUpto(msg)
|
||||
k.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Kafka) Stop() {
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
close(k.done)
|
||||
if err := k.Consumer.Close(); err != nil {
|
||||
log.Printf("Error closing kafka consumer: %s\n", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Kafka) Gather(acc inputs.Accumulator) error {
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
npoints := len(k.pointChan)
|
||||
for i := 0; i < npoints; i++ {
|
||||
point := <-k.pointChan
|
||||
acc.AddFields(point.Name(), point.Fields(), point.Tags(), point.Time())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("kafka_consumer", func() inputs.Input {
|
||||
return &Kafka{}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package kafka_consumer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReadsMetricsFromKafka(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
brokerPeers := []string{testutil.GetLocalHost() + ":9092"}
|
||||
zkPeers := []string{testutil.GetLocalHost() + ":2181"}
|
||||
testTopic := fmt.Sprintf("telegraf_test_topic_%d", time.Now().Unix())
|
||||
|
||||
// Send a Kafka message to the kafka host
|
||||
msg := "cpu_load_short,direction=in,host=server01,region=us-west value=23422.0 1422568543702900257"
|
||||
producer, err := sarama.NewSyncProducer(brokerPeers, nil)
|
||||
require.NoError(t, err)
|
||||
_, _, err = producer.SendMessage(
|
||||
&sarama.ProducerMessage{
|
||||
Topic: testTopic,
|
||||
Value: sarama.StringEncoder(msg),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer producer.Close()
|
||||
|
||||
// Start the Kafka Consumer
|
||||
k := &Kafka{
|
||||
ConsumerGroup: "telegraf_test_consumers",
|
||||
Topics: []string{testTopic},
|
||||
ZookeeperPeers: zkPeers,
|
||||
PointBuffer: 100000,
|
||||
Offset: "oldest",
|
||||
}
|
||||
if err := k.Start(); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
} else {
|
||||
defer k.Stop()
|
||||
}
|
||||
|
||||
waitForPoint(k, t)
|
||||
|
||||
// Verify that we can now gather the sent message
|
||||
var acc testutil.Accumulator
|
||||
// Sanity check
|
||||
assert.Equal(t, 0, len(acc.Points), "There should not be any points")
|
||||
|
||||
// Gather points
|
||||
err = k.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
if len(acc.Points) == 1 {
|
||||
point := acc.Points[0]
|
||||
assert.Equal(t, "cpu_load_short", point.Measurement)
|
||||
assert.Equal(t, map[string]interface{}{"value": 23422.0}, point.Fields)
|
||||
assert.Equal(t, map[string]string{
|
||||
"host": "server01",
|
||||
"direction": "in",
|
||||
"region": "us-west",
|
||||
}, point.Tags)
|
||||
assert.Equal(t, time.Unix(0, 1422568543702900257).Unix(), point.Time.Unix())
|
||||
} else {
|
||||
t.Errorf("No points found in accumulator, expected 1")
|
||||
}
|
||||
}
|
||||
|
||||
// Waits for the metric that was sent to the kafka broker to arrive at the kafka
|
||||
// consumer
|
||||
func waitForPoint(k *Kafka, t *testing.T) {
|
||||
// Give the kafka container up to 2 seconds to get the point to the consumer
|
||||
ticker := time.NewTicker(5 * time.Millisecond)
|
||||
counter := 0
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
counter++
|
||||
if counter > 1000 {
|
||||
t.Fatal("Waited for 5s, point never arrived to consumer")
|
||||
} else if len(k.pointChan) == 1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
plugins/inputs/kafka_consumer/kafka_consumer_test.go
Normal file
99
plugins/inputs/kafka_consumer/kafka_consumer_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package kafka_consumer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/influxdb/models"
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
testMsg = "cpu_load_short,host=server01 value=23422.0 1422568543702900257"
|
||||
invalidMsg = "cpu_load_short,host=server01 1422568543702900257"
|
||||
pointBuffer = 5
|
||||
)
|
||||
|
||||
func NewTestKafka() (*Kafka, chan *sarama.ConsumerMessage) {
|
||||
in := make(chan *sarama.ConsumerMessage, pointBuffer)
|
||||
k := Kafka{
|
||||
ConsumerGroup: "test",
|
||||
Topics: []string{"telegraf"},
|
||||
ZookeeperPeers: []string{"localhost:2181"},
|
||||
PointBuffer: pointBuffer,
|
||||
Offset: "oldest",
|
||||
in: in,
|
||||
doNotCommitMsgs: true,
|
||||
errs: make(chan *sarama.ConsumerError, pointBuffer),
|
||||
done: make(chan struct{}),
|
||||
pointChan: make(chan models.Point, pointBuffer),
|
||||
}
|
||||
return &k, in
|
||||
}
|
||||
|
||||
// Test that the parser parses kafka messages into points
|
||||
func TestRunParser(t *testing.T) {
|
||||
k, in := NewTestKafka()
|
||||
defer close(k.done)
|
||||
|
||||
go k.parser()
|
||||
in <- saramaMsg(testMsg)
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
assert.Equal(t, len(k.pointChan), 1)
|
||||
}
|
||||
|
||||
// Test that the parser ignores invalid messages
|
||||
func TestRunParserInvalidMsg(t *testing.T) {
|
||||
k, in := NewTestKafka()
|
||||
defer close(k.done)
|
||||
|
||||
go k.parser()
|
||||
in <- saramaMsg(invalidMsg)
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
assert.Equal(t, len(k.pointChan), 0)
|
||||
}
|
||||
|
||||
// Test that points are dropped when we hit the buffer limit
|
||||
func TestRunParserRespectsBuffer(t *testing.T) {
|
||||
k, in := NewTestKafka()
|
||||
defer close(k.done)
|
||||
|
||||
go k.parser()
|
||||
for i := 0; i < pointBuffer+1; i++ {
|
||||
in <- saramaMsg(testMsg)
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
assert.Equal(t, len(k.pointChan), 5)
|
||||
}
|
||||
|
||||
// Test that the parser parses kafka messages into points
|
||||
func TestRunParserAndGather(t *testing.T) {
|
||||
k, in := NewTestKafka()
|
||||
defer close(k.done)
|
||||
|
||||
go k.parser()
|
||||
in <- saramaMsg(testMsg)
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
acc := testutil.Accumulator{}
|
||||
k.Gather(&acc)
|
||||
|
||||
assert.Equal(t, len(acc.Points), 1)
|
||||
acc.AssertContainsFields(t, "cpu_load_short",
|
||||
map[string]interface{}{"value": float64(23422)})
|
||||
}
|
||||
|
||||
func saramaMsg(val string) *sarama.ConsumerMessage {
|
||||
return &sarama.ConsumerMessage{
|
||||
Key: nil,
|
||||
Value: []byte(val),
|
||||
Offset: 0,
|
||||
Partition: 0,
|
||||
}
|
||||
}
|
||||
231
plugins/inputs/leofs/leofs.go
Normal file
231
plugins/inputs/leofs/leofs.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package leofs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const oid = ".1.3.6.1.4.1.35450"
|
||||
|
||||
// For Manager Master
|
||||
const defaultEndpoint = "127.0.0.1:4020"
|
||||
|
||||
type ServerType int
|
||||
|
||||
const (
|
||||
ServerTypeManagerMaster ServerType = iota
|
||||
ServerTypeManagerSlave
|
||||
ServerTypeStorage
|
||||
ServerTypeGateway
|
||||
)
|
||||
|
||||
type LeoFS struct {
|
||||
Servers []string
|
||||
}
|
||||
|
||||
var KeyMapping = map[ServerType][]string{
|
||||
ServerTypeManagerMaster: {
|
||||
"num_of_processes",
|
||||
"total_memory_usage",
|
||||
"system_memory_usage",
|
||||
"processes_memory_usage",
|
||||
"ets_memory_usage",
|
||||
"num_of_processes_5min",
|
||||
"total_memory_usage_5min",
|
||||
"system_memory_usage_5min",
|
||||
"processes_memory_usage_5min",
|
||||
"ets_memory_usage_5min",
|
||||
"used_allocated_memory",
|
||||
"allocated_memory",
|
||||
"used_allocated_memory_5min",
|
||||
"allocated_memory_5min",
|
||||
},
|
||||
ServerTypeManagerSlave: {
|
||||
"num_of_processes",
|
||||
"total_memory_usage",
|
||||
"system_memory_usage",
|
||||
"processes_memory_usage",
|
||||
"ets_memory_usage",
|
||||
"num_of_processes_5min",
|
||||
"total_memory_usage_5min",
|
||||
"system_memory_usage_5min",
|
||||
"processes_memory_usage_5min",
|
||||
"ets_memory_usage_5min",
|
||||
"used_allocated_memory",
|
||||
"allocated_memory",
|
||||
"used_allocated_memory_5min",
|
||||
"allocated_memory_5min",
|
||||
},
|
||||
ServerTypeStorage: {
|
||||
"num_of_processes",
|
||||
"total_memory_usage",
|
||||
"system_memory_usage",
|
||||
"processes_memory_usage",
|
||||
"ets_memory_usage",
|
||||
"num_of_processes_5min",
|
||||
"total_memory_usage_5min",
|
||||
"system_memory_usage_5min",
|
||||
"processes_memory_usage_5min",
|
||||
"ets_memory_usage_5min",
|
||||
"num_of_writes",
|
||||
"num_of_reads",
|
||||
"num_of_deletes",
|
||||
"num_of_writes_5min",
|
||||
"num_of_reads_5min",
|
||||
"num_of_deletes_5min",
|
||||
"num_of_active_objects",
|
||||
"total_objects",
|
||||
"total_size_of_active_objects",
|
||||
"total_size",
|
||||
"num_of_replication_messages",
|
||||
"num_of_sync-vnode_messages",
|
||||
"num_of_rebalance_messages",
|
||||
"used_allocated_memory",
|
||||
"allocated_memory",
|
||||
"used_allocated_memory_5min",
|
||||
"allocated_memory_5min",
|
||||
},
|
||||
ServerTypeGateway: {
|
||||
"num_of_processes",
|
||||
"total_memory_usage",
|
||||
"system_memory_usage",
|
||||
"processes_memory_usage",
|
||||
"ets_memory_usage",
|
||||
"num_of_processes_5min",
|
||||
"total_memory_usage_5min",
|
||||
"system_memory_usage_5min",
|
||||
"processes_memory_usage_5min",
|
||||
"ets_memory_usage_5min",
|
||||
"num_of_writes",
|
||||
"num_of_reads",
|
||||
"num_of_deletes",
|
||||
"num_of_writes_5min",
|
||||
"num_of_reads_5min",
|
||||
"num_of_deletes_5min",
|
||||
"count_of_cache-hit",
|
||||
"count_of_cache-miss",
|
||||
"total_of_files",
|
||||
"total_cached_size",
|
||||
"used_allocated_memory",
|
||||
"allocated_memory",
|
||||
"used_allocated_memory_5min",
|
||||
"allocated_memory_5min",
|
||||
},
|
||||
}
|
||||
|
||||
var serverTypeMapping = map[string]ServerType{
|
||||
"4020": ServerTypeManagerMaster,
|
||||
"4021": ServerTypeManagerSlave,
|
||||
"4010": ServerTypeStorage,
|
||||
"4011": ServerTypeStorage,
|
||||
"4012": ServerTypeStorage,
|
||||
"4013": ServerTypeStorage,
|
||||
"4000": ServerTypeGateway,
|
||||
"4001": ServerTypeGateway,
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of URI to gather stats about LeoFS.
|
||||
# Specify an ip or hostname with port. ie 127.0.0.1:4020
|
||||
#
|
||||
# If no servers are specified, then 127.0.0.1 is used as the host and 4020 as the port.
|
||||
servers = ["127.0.0.1:4021"]
|
||||
`
|
||||
|
||||
func (l *LeoFS) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (l *LeoFS) Description() string {
|
||||
return "Read metrics from a LeoFS Server via SNMP"
|
||||
}
|
||||
|
||||
func (l *LeoFS) Gather(acc inputs.Accumulator) error {
|
||||
if len(l.Servers) == 0 {
|
||||
l.gatherServer(defaultEndpoint, ServerTypeManagerMaster, acc)
|
||||
return nil
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
var outerr error
|
||||
for _, endpoint := range l.Servers {
|
||||
_, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse the address:%s, err:%s", endpoint, err)
|
||||
}
|
||||
port, err := retrieveTokenAfterColon(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st, ok := serverTypeMapping[port]
|
||||
if !ok {
|
||||
st = ServerTypeStorage
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(endpoint string, st ServerType) {
|
||||
defer wg.Done()
|
||||
outerr = l.gatherServer(endpoint, st, acc)
|
||||
}(endpoint, st)
|
||||
}
|
||||
wg.Wait()
|
||||
return outerr
|
||||
}
|
||||
|
||||
func (l *LeoFS) gatherServer(endpoint string, serverType ServerType, acc inputs.Accumulator) error {
|
||||
cmd := exec.Command("snmpwalk", "-v2c", "-cpublic", endpoint, oid)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Start()
|
||||
defer cmd.Wait()
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
if !scanner.Scan() {
|
||||
return fmt.Errorf("Unable to retrieve the node name")
|
||||
}
|
||||
nodeName, err := retrieveTokenAfterColon(scanner.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nodeNameTrimmed := strings.Trim(nodeName, "\"")
|
||||
tags := map[string]string{
|
||||
"node": nodeNameTrimmed,
|
||||
}
|
||||
i := 0
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
for scanner.Scan() {
|
||||
key := KeyMapping[serverType][i]
|
||||
val, err := retrieveTokenAfterColon(scanner.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fVal, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse the value:%s, err:%s", val, err)
|
||||
}
|
||||
fields[key] = fVal
|
||||
i++
|
||||
}
|
||||
acc.AddFields("leofs", fields, tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
func retrieveTokenAfterColon(line string) (string, error) {
|
||||
tokens := strings.Split(line, ":")
|
||||
if len(tokens) != 2 {
|
||||
return "", fmt.Errorf("':' not found in the line:%s", line)
|
||||
}
|
||||
return strings.TrimSpace(tokens[1]), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("leofs", func() inputs.Input {
|
||||
return &LeoFS{}
|
||||
})
|
||||
}
|
||||
173
plugins/inputs/leofs/leofs_test.go
Normal file
173
plugins/inputs/leofs/leofs_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package leofs
|
||||
|
||||
import (
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var fakeSNMP4Manager = `
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
const output = ` + "`" + `iso.3.6.1.4.1.35450.15.1.0 = STRING: "manager_888@127.0.0.1"
|
||||
iso.3.6.1.4.1.35450.15.2.0 = Gauge32: 186
|
||||
iso.3.6.1.4.1.35450.15.3.0 = Gauge32: 46235519
|
||||
iso.3.6.1.4.1.35450.15.4.0 = Gauge32: 32168525
|
||||
iso.3.6.1.4.1.35450.15.5.0 = Gauge32: 14066068
|
||||
iso.3.6.1.4.1.35450.15.6.0 = Gauge32: 5512968
|
||||
iso.3.6.1.4.1.35450.15.7.0 = Gauge32: 186
|
||||
iso.3.6.1.4.1.35450.15.8.0 = Gauge32: 46269006
|
||||
iso.3.6.1.4.1.35450.15.9.0 = Gauge32: 32202867
|
||||
iso.3.6.1.4.1.35450.15.10.0 = Gauge32: 14064995
|
||||
iso.3.6.1.4.1.35450.15.11.0 = Gauge32: 5492634
|
||||
iso.3.6.1.4.1.35450.15.12.0 = Gauge32: 60
|
||||
iso.3.6.1.4.1.35450.15.13.0 = Gauge32: 43515904
|
||||
iso.3.6.1.4.1.35450.15.14.0 = Gauge32: 60
|
||||
iso.3.6.1.4.1.35450.15.15.0 = Gauge32: 43533983` + "`" +
|
||||
`
|
||||
func main() {
|
||||
fmt.Println(output)
|
||||
}
|
||||
`
|
||||
|
||||
var fakeSNMP4Storage = `
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
const output = ` + "`" + `iso.3.6.1.4.1.35450.34.1.0 = STRING: "storage_0@127.0.0.1"
|
||||
iso.3.6.1.4.1.35450.34.2.0 = Gauge32: 512
|
||||
iso.3.6.1.4.1.35450.34.3.0 = Gauge32: 38126307
|
||||
iso.3.6.1.4.1.35450.34.4.0 = Gauge32: 22308716
|
||||
iso.3.6.1.4.1.35450.34.5.0 = Gauge32: 15816448
|
||||
iso.3.6.1.4.1.35450.34.6.0 = Gauge32: 5232008
|
||||
iso.3.6.1.4.1.35450.34.7.0 = Gauge32: 512
|
||||
iso.3.6.1.4.1.35450.34.8.0 = Gauge32: 38113176
|
||||
iso.3.6.1.4.1.35450.34.9.0 = Gauge32: 22313398
|
||||
iso.3.6.1.4.1.35450.34.10.0 = Gauge32: 15798779
|
||||
iso.3.6.1.4.1.35450.34.11.0 = Gauge32: 5237315
|
||||
iso.3.6.1.4.1.35450.34.12.0 = Gauge32: 191
|
||||
iso.3.6.1.4.1.35450.34.13.0 = Gauge32: 824
|
||||
iso.3.6.1.4.1.35450.34.14.0 = Gauge32: 0
|
||||
iso.3.6.1.4.1.35450.34.15.0 = Gauge32: 50105
|
||||
iso.3.6.1.4.1.35450.34.16.0 = Gauge32: 196654
|
||||
iso.3.6.1.4.1.35450.34.17.0 = Gauge32: 0
|
||||
iso.3.6.1.4.1.35450.34.18.0 = Gauge32: 2052
|
||||
iso.3.6.1.4.1.35450.34.19.0 = Gauge32: 50296
|
||||
iso.3.6.1.4.1.35450.34.20.0 = Gauge32: 35
|
||||
iso.3.6.1.4.1.35450.34.21.0 = Gauge32: 898
|
||||
iso.3.6.1.4.1.35450.34.22.0 = Gauge32: 0
|
||||
iso.3.6.1.4.1.35450.34.23.0 = Gauge32: 0
|
||||
iso.3.6.1.4.1.35450.34.24.0 = Gauge32: 0
|
||||
iso.3.6.1.4.1.35450.34.31.0 = Gauge32: 51
|
||||
iso.3.6.1.4.1.35450.34.32.0 = Gauge32: 53219328
|
||||
iso.3.6.1.4.1.35450.34.33.0 = Gauge32: 51
|
||||
iso.3.6.1.4.1.35450.34.34.0 = Gauge32: 53351083` + "`" +
|
||||
`
|
||||
func main() {
|
||||
fmt.Println(output)
|
||||
}
|
||||
`
|
||||
|
||||
var fakeSNMP4Gateway = `
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
const output = ` + "`" + `iso.3.6.1.4.1.35450.34.1.0 = STRING: "gateway_0@127.0.0.1"
|
||||
iso.3.6.1.4.1.35450.34.2.0 = Gauge32: 465
|
||||
iso.3.6.1.4.1.35450.34.3.0 = Gauge32: 61676335
|
||||
iso.3.6.1.4.1.35450.34.4.0 = Gauge32: 46890415
|
||||
iso.3.6.1.4.1.35450.34.5.0 = Gauge32: 14785011
|
||||
iso.3.6.1.4.1.35450.34.6.0 = Gauge32: 5578855
|
||||
iso.3.6.1.4.1.35450.34.7.0 = Gauge32: 465
|
||||
iso.3.6.1.4.1.35450.34.8.0 = Gauge32: 61644426
|
||||
iso.3.6.1.4.1.35450.34.9.0 = Gauge32: 46880358
|
||||
iso.3.6.1.4.1.35450.34.10.0 = Gauge32: 14763002
|
||||
iso.3.6.1.4.1.35450.34.11.0 = Gauge32: 5582125
|
||||
iso.3.6.1.4.1.35450.34.12.0 = Gauge32: 191
|
||||
iso.3.6.1.4.1.35450.34.13.0 = Gauge32: 827
|
||||
iso.3.6.1.4.1.35450.34.14.0 = Gauge32: 0
|
||||
iso.3.6.1.4.1.35450.34.15.0 = Gauge32: 50105
|
||||
iso.3.6.1.4.1.35450.34.16.0 = Gauge32: 196650
|
||||
iso.3.6.1.4.1.35450.34.17.0 = Gauge32: 0
|
||||
iso.3.6.1.4.1.35450.34.18.0 = Gauge32: 30256
|
||||
iso.3.6.1.4.1.35450.34.19.0 = Gauge32: 532158
|
||||
iso.3.6.1.4.1.35450.34.20.0 = Gauge32: 34
|
||||
iso.3.6.1.4.1.35450.34.21.0 = Gauge32: 1
|
||||
iso.3.6.1.4.1.35450.34.31.0 = Gauge32: 53
|
||||
iso.3.6.1.4.1.35450.34.32.0 = Gauge32: 55050240
|
||||
iso.3.6.1.4.1.35450.34.33.0 = Gauge32: 53
|
||||
iso.3.6.1.4.1.35450.34.34.0 = Gauge32: 55186538` + "`" +
|
||||
`
|
||||
func main() {
|
||||
fmt.Println(output)
|
||||
}
|
||||
`
|
||||
|
||||
func makeFakeSNMPSrc(code string) string {
|
||||
path := os.TempDir() + "/test.go"
|
||||
err := ioutil.WriteFile(path, []byte(code), 0600)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func buildFakeSNMPCmd(src string) {
|
||||
err := exec.Command("go", "build", "-o", "snmpwalk", src).Run()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func testMain(t *testing.T, code string, endpoint string, serverType ServerType) {
|
||||
// Build the fake snmpwalk for test
|
||||
src := makeFakeSNMPSrc(code)
|
||||
defer os.Remove(src)
|
||||
buildFakeSNMPCmd(src)
|
||||
defer os.Remove("./snmpwalk")
|
||||
envPathOrigin := os.Getenv("PATH")
|
||||
// Refer to the fake snmpwalk
|
||||
os.Setenv("PATH", ".")
|
||||
defer os.Setenv("PATH", envPathOrigin)
|
||||
|
||||
l := &LeoFS{
|
||||
Servers: []string{endpoint},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
acc.SetDebug(true)
|
||||
|
||||
err := l.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
floatMetrics := KeyMapping[serverType]
|
||||
|
||||
for _, metric := range floatMetrics {
|
||||
assert.True(t, acc.HasFloatField("leofs", metric), metric)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeoFSManagerMasterMetrics(t *testing.T) {
|
||||
testMain(t, fakeSNMP4Manager, "localhost:4020", ServerTypeManagerMaster)
|
||||
}
|
||||
|
||||
func TestLeoFSManagerSlaveMetrics(t *testing.T) {
|
||||
testMain(t, fakeSNMP4Manager, "localhost:4021", ServerTypeManagerSlave)
|
||||
}
|
||||
|
||||
func TestLeoFSStorageMetrics(t *testing.T) {
|
||||
testMain(t, fakeSNMP4Storage, "localhost:4010", ServerTypeStorage)
|
||||
}
|
||||
|
||||
func TestLeoFSGatewayMetrics(t *testing.T) {
|
||||
testMain(t, fakeSNMP4Gateway, "localhost:4000", ServerTypeGateway)
|
||||
}
|
||||
250
plugins/inputs/lustre2/lustre2.go
Normal file
250
plugins/inputs/lustre2/lustre2.go
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
Lustre 2.x telegraf plugin
|
||||
|
||||
Lustre (http://lustre.org/) is an open-source, parallel file system
|
||||
for HPC environments. It stores statistics about its activity in
|
||||
/proc
|
||||
|
||||
*/
|
||||
package lustre2
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdb/telegraf/internal"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
// Lustre proc files can change between versions, so we want to future-proof
|
||||
// by letting people choose what to look at.
|
||||
type Lustre2 struct {
|
||||
Ost_procfiles []string
|
||||
Mds_procfiles []string
|
||||
|
||||
// allFields maps and OST name to the metric fields associated with that OST
|
||||
allFields map[string]map[string]interface{}
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of /proc globs to search for Lustre stats
|
||||
# If not specified, the default will work on Lustre 2.5.x
|
||||
#
|
||||
# ost_procfiles = ["/proc/fs/lustre/obdfilter/*/stats", "/proc/fs/lustre/osd-ldiskfs/*/stats"]
|
||||
# mds_procfiles = ["/proc/fs/lustre/mdt/*/md_stats"]
|
||||
`
|
||||
|
||||
/* The wanted fields would be a []string if not for the
|
||||
lines that start with read_bytes/write_bytes and contain
|
||||
both the byte count and the function call count
|
||||
*/
|
||||
type mapping struct {
|
||||
inProc string // What to look for at the start of a line in /proc/fs/lustre/*
|
||||
field uint32 // which field to extract from that line
|
||||
reportAs string // What measurement name to use
|
||||
tag string // Additional tag to add for this metric
|
||||
}
|
||||
|
||||
var wanted_ost_fields = []*mapping{
|
||||
{
|
||||
inProc: "write_bytes",
|
||||
field: 6,
|
||||
reportAs: "write_bytes",
|
||||
},
|
||||
{ // line starts with 'write_bytes', but value write_calls is in second column
|
||||
inProc: "write_bytes",
|
||||
field: 1,
|
||||
reportAs: "write_calls",
|
||||
},
|
||||
{
|
||||
inProc: "read_bytes",
|
||||
field: 6,
|
||||
reportAs: "read_bytes",
|
||||
},
|
||||
{ // line starts with 'read_bytes', but value read_calls is in second column
|
||||
inProc: "read_bytes",
|
||||
field: 1,
|
||||
reportAs: "read_calls",
|
||||
},
|
||||
{
|
||||
inProc: "cache_hit",
|
||||
},
|
||||
{
|
||||
inProc: "cache_miss",
|
||||
},
|
||||
{
|
||||
inProc: "cache_access",
|
||||
},
|
||||
}
|
||||
|
||||
var wanted_mds_fields = []*mapping{
|
||||
{
|
||||
inProc: "open",
|
||||
},
|
||||
{
|
||||
inProc: "close",
|
||||
},
|
||||
{
|
||||
inProc: "mknod",
|
||||
},
|
||||
{
|
||||
inProc: "link",
|
||||
},
|
||||
{
|
||||
inProc: "unlink",
|
||||
},
|
||||
{
|
||||
inProc: "mkdir",
|
||||
},
|
||||
{
|
||||
inProc: "rmdir",
|
||||
},
|
||||
{
|
||||
inProc: "rename",
|
||||
},
|
||||
{
|
||||
inProc: "getattr",
|
||||
},
|
||||
{
|
||||
inProc: "setattr",
|
||||
},
|
||||
{
|
||||
inProc: "getxattr",
|
||||
},
|
||||
{
|
||||
inProc: "setxattr",
|
||||
},
|
||||
{
|
||||
inProc: "statfs",
|
||||
},
|
||||
{
|
||||
inProc: "sync",
|
||||
},
|
||||
{
|
||||
inProc: "samedir_rename",
|
||||
},
|
||||
{
|
||||
inProc: "crossdir_rename",
|
||||
},
|
||||
}
|
||||
|
||||
func (l *Lustre2) GetLustreProcStats(fileglob string, wanted_fields []*mapping, acc inputs.Accumulator) error {
|
||||
files, err := filepath.Glob(fileglob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
/* Turn /proc/fs/lustre/obdfilter/<ost_name>/stats and similar
|
||||
* into just the object store target name
|
||||
* Assumpion: the target name is always second to last,
|
||||
* which is true in Lustre 2.1->2.5
|
||||
*/
|
||||
path := strings.Split(file, "/")
|
||||
name := path[len(path)-2]
|
||||
var fields map[string]interface{}
|
||||
fields, ok := l.allFields[name]
|
||||
if !ok {
|
||||
fields = make(map[string]interface{})
|
||||
l.allFields[name] = fields
|
||||
}
|
||||
|
||||
lines, err := internal.ReadLines(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
for _, wanted := range wanted_fields {
|
||||
var data uint64
|
||||
if parts[0] == wanted.inProc {
|
||||
wanted_field := wanted.field
|
||||
// if not set, assume field[1]. Shouldn't be field[0], as
|
||||
// that's a string
|
||||
if wanted_field == 0 {
|
||||
wanted_field = 1
|
||||
}
|
||||
data, err = strconv.ParseUint((parts[wanted_field]), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report_name := wanted.inProc
|
||||
if wanted.reportAs != "" {
|
||||
report_name = wanted.reportAs
|
||||
}
|
||||
fields[report_name] = data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SampleConfig returns sample configuration message
|
||||
func (l *Lustre2) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
// Description returns description of Lustre2 plugin
|
||||
func (l *Lustre2) Description() string {
|
||||
return "Read metrics from local Lustre service on OST, MDS"
|
||||
}
|
||||
|
||||
// Gather reads stats from all lustre targets
|
||||
func (l *Lustre2) Gather(acc inputs.Accumulator) error {
|
||||
l.allFields = make(map[string]map[string]interface{})
|
||||
|
||||
if len(l.Ost_procfiles) == 0 {
|
||||
// read/write bytes are in obdfilter/<ost_name>/stats
|
||||
err := l.GetLustreProcStats("/proc/fs/lustre/obdfilter/*/stats",
|
||||
wanted_ost_fields, acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// cache counters are in osd-ldiskfs/<ost_name>/stats
|
||||
err = l.GetLustreProcStats("/proc/fs/lustre/osd-ldiskfs/*/stats",
|
||||
wanted_ost_fields, acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(l.Mds_procfiles) == 0 {
|
||||
// Metadata server stats
|
||||
err := l.GetLustreProcStats("/proc/fs/lustre/mdt/*/md_stats",
|
||||
wanted_mds_fields, acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, procfile := range l.Ost_procfiles {
|
||||
err := l.GetLustreProcStats(procfile, wanted_ost_fields, acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, procfile := range l.Mds_procfiles {
|
||||
err := l.GetLustreProcStats(procfile, wanted_mds_fields, acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for name, fields := range l.allFields {
|
||||
tags := map[string]string{
|
||||
"name": name,
|
||||
}
|
||||
acc.AddFields("lustre2", fields, tags)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("lustre2", func() inputs.Input {
|
||||
return &Lustre2{}
|
||||
})
|
||||
}
|
||||
130
plugins/inputs/lustre2/lustre2_test.go
Normal file
130
plugins/inputs/lustre2/lustre2_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package lustre2
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Set config file variables to point to fake directory structure instead of /proc?
|
||||
|
||||
const obdfilterProcContents = `snapshot_time 1438693064.430544 secs.usecs
|
||||
read_bytes 203238095 samples [bytes] 4096 1048576 78026117632000
|
||||
write_bytes 71893382 samples [bytes] 1 1048576 15201500833981
|
||||
get_info 1182008495 samples [reqs]
|
||||
set_info_async 2 samples [reqs]
|
||||
connect 1117 samples [reqs]
|
||||
reconnect 1160 samples [reqs]
|
||||
disconnect 1084 samples [reqs]
|
||||
statfs 3575885 samples [reqs]
|
||||
create 698 samples [reqs]
|
||||
destroy 3190060 samples [reqs]
|
||||
setattr 605647 samples [reqs]
|
||||
punch 805187 samples [reqs]
|
||||
sync 6608753 samples [reqs]
|
||||
preprw 275131477 samples [reqs]
|
||||
commitrw 275131477 samples [reqs]
|
||||
quotactl 229231 samples [reqs]
|
||||
ping 78020757 samples [reqs]
|
||||
`
|
||||
|
||||
const osdldiskfsProcContents = `snapshot_time 1438693135.640551 secs.usecs
|
||||
get_page 275132812 samples [usec] 0 3147 1320420955 22041662259
|
||||
cache_access 19047063027 samples [pages] 1 1 19047063027
|
||||
cache_hit 7393729777 samples [pages] 1 1 7393729777
|
||||
cache_miss 11653333250 samples [pages] 1 1 11653333250
|
||||
`
|
||||
|
||||
const mdtProcContents = `snapshot_time 1438693238.20113 secs.usecs
|
||||
open 1024577037 samples [reqs]
|
||||
close 873243496 samples [reqs]
|
||||
mknod 349042 samples [reqs]
|
||||
link 445 samples [reqs]
|
||||
unlink 3549417 samples [reqs]
|
||||
mkdir 705499 samples [reqs]
|
||||
rmdir 227434 samples [reqs]
|
||||
rename 629196 samples [reqs]
|
||||
getattr 1503663097 samples [reqs]
|
||||
setattr 1898364 samples [reqs]
|
||||
getxattr 6145349681 samples [reqs]
|
||||
setxattr 83969 samples [reqs]
|
||||
statfs 2916320 samples [reqs]
|
||||
sync 434081 samples [reqs]
|
||||
samedir_rename 259625 samples [reqs]
|
||||
crossdir_rename 369571 samples [reqs]
|
||||
`
|
||||
|
||||
func TestLustre2GeneratesMetrics(t *testing.T) {
|
||||
|
||||
tempdir := os.TempDir() + "/telegraf/proc/fs/lustre/"
|
||||
ost_name := "OST0001"
|
||||
|
||||
mdtdir := tempdir + "/mdt/"
|
||||
err := os.MkdirAll(mdtdir+"/"+ost_name, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
osddir := tempdir + "/osd-ldiskfs/"
|
||||
err = os.MkdirAll(osddir+"/"+ost_name, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
obddir := tempdir + "/obdfilter/"
|
||||
err = os.MkdirAll(obddir+"/"+ost_name, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(mdtdir+"/"+ost_name+"/md_stats", []byte(mdtProcContents), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(osddir+"/"+ost_name+"/stats", []byte(osdldiskfsProcContents), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ioutil.WriteFile(obddir+"/"+ost_name+"/stats", []byte(obdfilterProcContents), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := &Lustre2{
|
||||
Ost_procfiles: []string{obddir + "/*/stats", osddir + "/*/stats"},
|
||||
Mds_procfiles: []string{mdtdir + "/*/md_stats"},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err = m.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags := map[string]string{
|
||||
"name": ost_name,
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"cache_access": uint64(19047063027),
|
||||
"cache_hit": uint64(7393729777),
|
||||
"cache_miss": uint64(11653333250),
|
||||
"close": uint64(873243496),
|
||||
"crossdir_rename": uint64(369571),
|
||||
"getattr": uint64(1503663097),
|
||||
"getxattr": uint64(6145349681),
|
||||
"link": uint64(445),
|
||||
"mkdir": uint64(705499),
|
||||
"mknod": uint64(349042),
|
||||
"open": uint64(1024577037),
|
||||
"read_bytes": uint64(78026117632000),
|
||||
"read_calls": uint64(203238095),
|
||||
"rename": uint64(629196),
|
||||
"rmdir": uint64(227434),
|
||||
"samedir_rename": uint64(259625),
|
||||
"setattr": uint64(1898364),
|
||||
"setxattr": uint64(83969),
|
||||
"statfs": uint64(2916320),
|
||||
"sync": uint64(434081),
|
||||
"unlink": uint64(3549417),
|
||||
"write_bytes": uint64(15201500833981),
|
||||
"write_calls": uint64(71893382),
|
||||
}
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "lustre2", fields, tags)
|
||||
|
||||
err = os.RemoveAll(os.TempDir() + "/telegraf")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
234
plugins/inputs/mailchimp/chimp_api.go
Normal file
234
plugins/inputs/mailchimp/chimp_api.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package mailchimp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
reports_endpoint string = "/3.0/reports"
|
||||
reports_endpoint_campaign string = "/3.0/reports/%s"
|
||||
)
|
||||
|
||||
var mailchimp_datacenter = regexp.MustCompile("[a-z]+[0-9]+$")
|
||||
|
||||
type ChimpAPI struct {
|
||||
Transport http.RoundTripper
|
||||
Debug bool
|
||||
|
||||
sync.Mutex
|
||||
|
||||
url *url.URL
|
||||
}
|
||||
|
||||
type ReportsParams struct {
|
||||
Count string
|
||||
Offset string
|
||||
SinceSendTime string
|
||||
BeforeSendTime string
|
||||
}
|
||||
|
||||
func (p *ReportsParams) String() string {
|
||||
v := url.Values{}
|
||||
if p.Count != "" {
|
||||
v.Set("count", p.Count)
|
||||
}
|
||||
if p.Offset != "" {
|
||||
v.Set("offset", p.Offset)
|
||||
}
|
||||
if p.BeforeSendTime != "" {
|
||||
v.Set("before_send_time", p.BeforeSendTime)
|
||||
}
|
||||
if p.SinceSendTime != "" {
|
||||
v.Set("since_send_time", p.SinceSendTime)
|
||||
}
|
||||
return v.Encode()
|
||||
}
|
||||
|
||||
func NewChimpAPI(apiKey string) *ChimpAPI {
|
||||
u := &url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = fmt.Sprintf("%s.api.mailchimp.com", mailchimp_datacenter.FindString(apiKey))
|
||||
u.User = url.UserPassword("", apiKey)
|
||||
return &ChimpAPI{url: u}
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Status int `json:"status"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Detail string `json:"detail"`
|
||||
Instance string `json:"instance"`
|
||||
}
|
||||
|
||||
func (e APIError) Error() string {
|
||||
return fmt.Sprintf("ERROR %v: %v. See %v", e.Status, e.Title, e.Type)
|
||||
}
|
||||
|
||||
func chimpErrorCheck(body []byte) error {
|
||||
var e APIError
|
||||
json.Unmarshal(body, &e)
|
||||
if e.Title != "" || e.Status != 0 {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ChimpAPI) GetReports(params ReportsParams) (ReportsResponse, error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
a.url.Path = reports_endpoint
|
||||
|
||||
var response ReportsResponse
|
||||
rawjson, err := runChimp(a, params)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(rawjson, &response)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *ChimpAPI) GetReport(campaignID string) (Report, error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
a.url.Path = fmt.Sprintf(reports_endpoint_campaign, campaignID)
|
||||
|
||||
var response Report
|
||||
rawjson, err := runChimp(a, ReportsParams{})
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(rawjson, &response)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func runChimp(api *ChimpAPI, params ReportsParams) ([]byte, error) {
|
||||
client := &http.Client{Transport: api.Transport}
|
||||
|
||||
var b bytes.Buffer
|
||||
req, err := http.NewRequest("GET", api.url.String(), &b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.URL.RawQuery = params.String()
|
||||
req.Header.Set("User-Agent", "Telegraf-MailChimp-Plugin")
|
||||
if api.Debug {
|
||||
log.Printf("Request URL: %s", req.URL.String())
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if api.Debug {
|
||||
log.Printf("Response Body:%s", string(body))
|
||||
}
|
||||
|
||||
if err = chimpErrorCheck(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
type ReportsResponse struct {
|
||||
Reports []Report `json:"reports"`
|
||||
TotalItems int `json:"total_items"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
ID string `json:"id"`
|
||||
CampaignTitle string `json:"campaign_title"`
|
||||
Type string `json:"type"`
|
||||
EmailsSent int `json:"emails_sent"`
|
||||
AbuseReports int `json:"abuse_reports"`
|
||||
Unsubscribed int `json:"unsubscribed"`
|
||||
SendTime string `json:"send_time"`
|
||||
|
||||
TimeSeries []TimeSerie
|
||||
Bounces Bounces `json:"bounces"`
|
||||
Forwards Forwards `json:"forwards"`
|
||||
Opens Opens `json:"opens"`
|
||||
Clicks Clicks `json:"clicks"`
|
||||
FacebookLikes FacebookLikes `json:"facebook_likes"`
|
||||
IndustryStats IndustryStats `json:"industry_stats"`
|
||||
ListStats ListStats `json:"list_stats"`
|
||||
}
|
||||
|
||||
type Bounces struct {
|
||||
HardBounces int `json:"hard_bounces"`
|
||||
SoftBounces int `json:"soft_bounces"`
|
||||
SyntaxErrors int `json:"syntax_errors"`
|
||||
}
|
||||
|
||||
type Forwards struct {
|
||||
ForwardsCount int `json:"forwards_count"`
|
||||
ForwardsOpens int `json:"forwards_opens"`
|
||||
}
|
||||
|
||||
type Opens struct {
|
||||
OpensTotal int `json:"opens_total"`
|
||||
UniqueOpens int `json:"unique_opens"`
|
||||
OpenRate float64 `json:"open_rate"`
|
||||
LastOpen string `json:"last_open"`
|
||||
}
|
||||
|
||||
type Clicks struct {
|
||||
ClicksTotal int `json:"clicks_total"`
|
||||
UniqueClicks int `json:"unique_clicks"`
|
||||
UniqueSubscriberClicks int `json:"unique_subscriber_clicks"`
|
||||
ClickRate float64 `json:"click_rate"`
|
||||
LastClick string `json:"last_click"`
|
||||
}
|
||||
|
||||
type FacebookLikes struct {
|
||||
RecipientLikes int `json:"recipient_likes"`
|
||||
UniqueLikes int `json:"unique_likes"`
|
||||
FacebookLikes int `json:"facebook_likes"`
|
||||
}
|
||||
|
||||
type IndustryStats struct {
|
||||
Type string `json:"type"`
|
||||
OpenRate float64 `json:"open_rate"`
|
||||
ClickRate float64 `json:"click_rate"`
|
||||
BounceRate float64 `json:"bounce_rate"`
|
||||
UnopenRate float64 `json:"unopen_rate"`
|
||||
UnsubRate float64 `json:"unsub_rate"`
|
||||
AbuseRate float64 `json:"abuse_rate"`
|
||||
}
|
||||
|
||||
type ListStats struct {
|
||||
SubRate float64 `json:"sub_rate"`
|
||||
UnsubRate float64 `json:"unsub_rate"`
|
||||
OpenRate float64 `json:"open_rate"`
|
||||
ClickRate float64 `json:"click_rate"`
|
||||
}
|
||||
|
||||
type TimeSerie struct {
|
||||
TimeStamp string `json:"timestamp"`
|
||||
EmailsSent int `json:"emails_sent"`
|
||||
UniqueOpens int `json:"unique_opens"`
|
||||
RecipientsClick int `json:"recipients_click"`
|
||||
}
|
||||
116
plugins/inputs/mailchimp/mailchimp.go
Normal file
116
plugins/inputs/mailchimp/mailchimp.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package mailchimp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type MailChimp struct {
|
||||
api *ChimpAPI
|
||||
|
||||
ApiKey string
|
||||
DaysOld int
|
||||
CampaignId string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# MailChimp API key
|
||||
# get from https://admin.mailchimp.com/account/api/
|
||||
api_key = "" # required
|
||||
# Reports for campaigns sent more than days_old ago will not be collected.
|
||||
# 0 means collect all.
|
||||
days_old = 0
|
||||
# Campaign ID to get, if empty gets all campaigns, this option overrides days_old
|
||||
# campaign_id = ""
|
||||
`
|
||||
|
||||
func (m *MailChimp) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (m *MailChimp) Description() string {
|
||||
return "Gathers metrics from the /3.0/reports MailChimp API"
|
||||
}
|
||||
|
||||
func (m *MailChimp) Gather(acc inputs.Accumulator) error {
|
||||
if m.api == nil {
|
||||
m.api = NewChimpAPI(m.ApiKey)
|
||||
}
|
||||
m.api.Debug = false
|
||||
|
||||
if m.CampaignId == "" {
|
||||
since := ""
|
||||
if m.DaysOld > 0 {
|
||||
now := time.Now()
|
||||
d, _ := time.ParseDuration(fmt.Sprintf("%dh", 24*m.DaysOld))
|
||||
since = now.Add(-d).Format(time.RFC3339)
|
||||
}
|
||||
|
||||
reports, err := m.api.GetReports(ReportsParams{
|
||||
SinceSendTime: since,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
for _, report := range reports.Reports {
|
||||
gatherReport(acc, report, now)
|
||||
}
|
||||
} else {
|
||||
report, err := m.api.GetReport(m.CampaignId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
gatherReport(acc, report, now)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gatherReport(acc inputs.Accumulator, report Report, now time.Time) {
|
||||
tags := make(map[string]string)
|
||||
tags["id"] = report.ID
|
||||
tags["campaign_title"] = report.CampaignTitle
|
||||
fields := map[string]interface{}{
|
||||
"emails_sent": report.EmailsSent,
|
||||
"abuse_reports": report.AbuseReports,
|
||||
"unsubscribed": report.Unsubscribed,
|
||||
"hard_bounces": report.Bounces.HardBounces,
|
||||
"soft_bounces": report.Bounces.SoftBounces,
|
||||
"syntax_errors": report.Bounces.SyntaxErrors,
|
||||
"forwards_count": report.Forwards.ForwardsCount,
|
||||
"forwards_opens": report.Forwards.ForwardsOpens,
|
||||
"opens_total": report.Opens.OpensTotal,
|
||||
"unique_opens": report.Opens.UniqueOpens,
|
||||
"open_rate": report.Opens.OpenRate,
|
||||
"clicks_total": report.Clicks.ClicksTotal,
|
||||
"unique_clicks": report.Clicks.UniqueClicks,
|
||||
"unique_subscriber_clicks": report.Clicks.UniqueSubscriberClicks,
|
||||
"click_rate": report.Clicks.ClickRate,
|
||||
"facebook_recipient_likes": report.FacebookLikes.RecipientLikes,
|
||||
"facebook_unique_likes": report.FacebookLikes.UniqueLikes,
|
||||
"facebook_likes": report.FacebookLikes.FacebookLikes,
|
||||
"industry_type": report.IndustryStats.Type,
|
||||
"industry_open_rate": report.IndustryStats.OpenRate,
|
||||
"industry_click_rate": report.IndustryStats.ClickRate,
|
||||
"industry_bounce_rate": report.IndustryStats.BounceRate,
|
||||
"industry_unopen_rate": report.IndustryStats.UnopenRate,
|
||||
"industry_unsub_rate": report.IndustryStats.UnsubRate,
|
||||
"industry_abuse_rate": report.IndustryStats.AbuseRate,
|
||||
"list_stats_sub_rate": report.ListStats.SubRate,
|
||||
"list_stats_unsub_rate": report.ListStats.UnsubRate,
|
||||
"list_stats_open_rate": report.ListStats.OpenRate,
|
||||
"list_stats_click_rate": report.ListStats.ClickRate,
|
||||
}
|
||||
acc.AddFields("mailchimp", fields, tags, now)
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("mailchimp", func() inputs.Input {
|
||||
return &MailChimp{}
|
||||
})
|
||||
}
|
||||
774
plugins/inputs/mailchimp/mailchimp_test.go
Normal file
774
plugins/inputs/mailchimp/mailchimp_test.go
Normal file
@@ -0,0 +1,774 @@
|
||||
package mailchimp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMailChimpGatherReports(t *testing.T) {
|
||||
ts := httptest.NewServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, sampleReports)
|
||||
},
|
||||
))
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.ParseRequestURI(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := &ChimpAPI{
|
||||
url: u,
|
||||
Debug: true,
|
||||
}
|
||||
m := MailChimp{
|
||||
api: api,
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err = m.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags := make(map[string]string)
|
||||
tags["id"] = "42694e9e57"
|
||||
tags["campaign_title"] = "Freddie's Jokes Vol. 1"
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"emails_sent": int(200),
|
||||
"abuse_reports": int(0),
|
||||
"unsubscribed": int(2),
|
||||
"hard_bounces": int(0),
|
||||
"soft_bounces": int(2),
|
||||
"syntax_errors": int(0),
|
||||
"forwards_count": int(0),
|
||||
"forwards_opens": int(0),
|
||||
"opens_total": int(186),
|
||||
"unique_opens": int(100),
|
||||
"clicks_total": int(42),
|
||||
"unique_clicks": int(400),
|
||||
"unique_subscriber_clicks": int(42),
|
||||
"facebook_recipient_likes": int(5),
|
||||
"facebook_unique_likes": int(8),
|
||||
"facebook_likes": int(42),
|
||||
"open_rate": float64(42),
|
||||
"click_rate": float64(42),
|
||||
"industry_open_rate": float64(0.17076777144396),
|
||||
"industry_click_rate": float64(0.027431311866951),
|
||||
"industry_bounce_rate": float64(0.0063767751251474),
|
||||
"industry_unopen_rate": float64(0.82285545343089),
|
||||
"industry_unsub_rate": float64(0.001436957032815),
|
||||
"industry_abuse_rate": float64(0.00021111996110887),
|
||||
"list_stats_sub_rate": float64(10),
|
||||
"list_stats_unsub_rate": float64(20),
|
||||
"list_stats_open_rate": float64(42),
|
||||
"list_stats_click_rate": float64(42),
|
||||
"industry_type": "Social Networks and Online Communities",
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "mailchimp", fields, tags)
|
||||
}
|
||||
|
||||
func TestMailChimpGatherReport(t *testing.T) {
|
||||
ts := httptest.NewServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, sampleReport)
|
||||
},
|
||||
))
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.ParseRequestURI(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := &ChimpAPI{
|
||||
url: u,
|
||||
Debug: true,
|
||||
}
|
||||
m := MailChimp{
|
||||
api: api,
|
||||
CampaignId: "test",
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err = m.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags := make(map[string]string)
|
||||
tags["id"] = "42694e9e57"
|
||||
tags["campaign_title"] = "Freddie's Jokes Vol. 1"
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"emails_sent": int(200),
|
||||
"abuse_reports": int(0),
|
||||
"unsubscribed": int(2),
|
||||
"hard_bounces": int(0),
|
||||
"soft_bounces": int(2),
|
||||
"syntax_errors": int(0),
|
||||
"forwards_count": int(0),
|
||||
"forwards_opens": int(0),
|
||||
"opens_total": int(186),
|
||||
"unique_opens": int(100),
|
||||
"clicks_total": int(42),
|
||||
"unique_clicks": int(400),
|
||||
"unique_subscriber_clicks": int(42),
|
||||
"facebook_recipient_likes": int(5),
|
||||
"facebook_unique_likes": int(8),
|
||||
"facebook_likes": int(42),
|
||||
"open_rate": float64(42),
|
||||
"click_rate": float64(42),
|
||||
"industry_open_rate": float64(0.17076777144396),
|
||||
"industry_click_rate": float64(0.027431311866951),
|
||||
"industry_bounce_rate": float64(0.0063767751251474),
|
||||
"industry_unopen_rate": float64(0.82285545343089),
|
||||
"industry_unsub_rate": float64(0.001436957032815),
|
||||
"industry_abuse_rate": float64(0.00021111996110887),
|
||||
"list_stats_sub_rate": float64(10),
|
||||
"list_stats_unsub_rate": float64(20),
|
||||
"list_stats_open_rate": float64(42),
|
||||
"list_stats_click_rate": float64(42),
|
||||
"industry_type": "Social Networks and Online Communities",
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "mailchimp", fields, tags)
|
||||
|
||||
}
|
||||
|
||||
func TestMailChimpGatherError(t *testing.T) {
|
||||
ts := httptest.NewServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, sampleError)
|
||||
},
|
||||
))
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.ParseRequestURI(ts.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
api := &ChimpAPI{
|
||||
url: u,
|
||||
Debug: true,
|
||||
}
|
||||
m := MailChimp{
|
||||
api: api,
|
||||
CampaignId: "test",
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err = m.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
var sampleReports = `
|
||||
{
|
||||
"reports": [
|
||||
{
|
||||
"id": "42694e9e57",
|
||||
"campaign_title": "Freddie's Jokes Vol. 1",
|
||||
"type": "regular",
|
||||
"emails_sent": 200,
|
||||
"abuse_reports": 0,
|
||||
"unsubscribed": 2,
|
||||
"send_time": "2015-09-15T19:05:51+00:00",
|
||||
"bounces": {
|
||||
"hard_bounces": 0,
|
||||
"soft_bounces": 2,
|
||||
"syntax_errors": 0
|
||||
},
|
||||
"forwards": {
|
||||
"forwards_count": 0,
|
||||
"forwards_opens": 0
|
||||
},
|
||||
"opens": {
|
||||
"opens_total": 186,
|
||||
"unique_opens": 100,
|
||||
"open_rate": 42,
|
||||
"last_open": "2015-09-15T19:15:47+00:00"
|
||||
},
|
||||
"clicks": {
|
||||
"clicks_total": 42,
|
||||
"unique_clicks": 400,
|
||||
"unique_subscriber_clicks": 42,
|
||||
"click_rate": 42,
|
||||
"last_click": "2015-09-15T19:15:47+00:00"
|
||||
},
|
||||
"facebook_likes": {
|
||||
"recipient_likes": 5,
|
||||
"unique_likes": 8,
|
||||
"facebook_likes": 42
|
||||
},
|
||||
"industry_stats": {
|
||||
"type": "Social Networks and Online Communities",
|
||||
"open_rate": 0.17076777144396,
|
||||
"click_rate": 0.027431311866951,
|
||||
"bounce_rate": 0.0063767751251474,
|
||||
"unopen_rate": 0.82285545343089,
|
||||
"unsub_rate": 0.001436957032815,
|
||||
"abuse_rate": 0.00021111996110887
|
||||
},
|
||||
"list_stats": {
|
||||
"sub_rate": 10,
|
||||
"unsub_rate": 20,
|
||||
"open_rate": 42,
|
||||
"click_rate": 42
|
||||
},
|
||||
"timeseries": [
|
||||
{
|
||||
"timestamp": "2015-09-15T19:00:00+00:00",
|
||||
"emails_sent": 198,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-15T20:00:00+00:00",
|
||||
"emails_sent": 2,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-15T21:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-15T22:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-15T23:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T00:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T01:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T02:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T03:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T04:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T05:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T06:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T07:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T08:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T09:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T10:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T11:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T12:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T13:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T14:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T15:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T16:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T17:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T18:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
}
|
||||
],
|
||||
"share_report": {
|
||||
"share_url": "http://usX.vip-reports.net/reports/summary?u=xxxx&id=xxxx",
|
||||
"share_password": "freddielikesjokes"
|
||||
},
|
||||
"delivery_status": {
|
||||
"enabled": false
|
||||
},
|
||||
"_links": [
|
||||
{
|
||||
"rel": "parent",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Collection.json",
|
||||
"schema": "https://api.mailchimp.com/schema/3.0/CollectionLinks/Reports.json"
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Instance.json"
|
||||
},
|
||||
{
|
||||
"rel": "campaign",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/campaigns/42694e9e57",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Campaigns/Instance.json"
|
||||
},
|
||||
{
|
||||
"rel": "sub-reports",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/sub-reports",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Sub/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "abuse-reports",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/abuse-reports",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Abuse/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "advice",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/advice",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Advice/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "click-details",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/click-details",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/ClickDetails/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "domain-performance",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/domain-performance",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/DomainPerformance/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "eepurl",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/eepurl",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Eepurl/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "email-activity",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/email-activity",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/EmailActivity/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "locations",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/locations",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Locations/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "sent-to",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/sent-to",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/SentTo/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "unsubscribed",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/unsubscribed",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Unsubs/Collection.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"_links": [
|
||||
{
|
||||
"rel": "parent",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Root.json"
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Collection.json",
|
||||
"schema": "https://api.mailchimp.com/schema/3.0/CollectionLinks/Reports.json"
|
||||
}
|
||||
],
|
||||
"total_items": 1
|
||||
}
|
||||
`
|
||||
|
||||
var sampleReport = `
|
||||
{
|
||||
"id": "42694e9e57",
|
||||
"campaign_title": "Freddie's Jokes Vol. 1",
|
||||
"type": "regular",
|
||||
"emails_sent": 200,
|
||||
"abuse_reports": 0,
|
||||
"unsubscribed": 2,
|
||||
"send_time": "2015-09-15T19:05:51+00:00",
|
||||
"bounces": {
|
||||
"hard_bounces": 0,
|
||||
"soft_bounces": 2,
|
||||
"syntax_errors": 0
|
||||
},
|
||||
"forwards": {
|
||||
"forwards_count": 0,
|
||||
"forwards_opens": 0
|
||||
},
|
||||
"opens": {
|
||||
"opens_total": 186,
|
||||
"unique_opens": 100,
|
||||
"open_rate": 42,
|
||||
"last_open": "2015-09-15T19:15:47+00:00"
|
||||
},
|
||||
"clicks": {
|
||||
"clicks_total": 42,
|
||||
"unique_clicks": 400,
|
||||
"unique_subscriber_clicks": 42,
|
||||
"click_rate": 42,
|
||||
"last_click": "2015-09-15T19:15:47+00:00"
|
||||
},
|
||||
"facebook_likes": {
|
||||
"recipient_likes": 5,
|
||||
"unique_likes": 8,
|
||||
"facebook_likes": 42
|
||||
},
|
||||
"industry_stats": {
|
||||
"type": "Social Networks and Online Communities",
|
||||
"open_rate": 0.17076777144396,
|
||||
"click_rate": 0.027431311866951,
|
||||
"bounce_rate": 0.0063767751251474,
|
||||
"unopen_rate": 0.82285545343089,
|
||||
"unsub_rate": 0.001436957032815,
|
||||
"abuse_rate": 0.00021111996110887
|
||||
},
|
||||
"list_stats": {
|
||||
"sub_rate": 10,
|
||||
"unsub_rate": 20,
|
||||
"open_rate": 42,
|
||||
"click_rate": 42
|
||||
},
|
||||
"timeseries": [
|
||||
{
|
||||
"timestamp": "2015-09-15T19:00:00+00:00",
|
||||
"emails_sent": 198,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-15T20:00:00+00:00",
|
||||
"emails_sent": 2,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-15T21:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-15T22:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-15T23:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T00:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T01:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T02:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T03:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T04:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T05:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T06:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T07:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T08:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T09:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T10:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T11:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T12:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T13:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T14:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T15:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T16:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T17:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
},
|
||||
{
|
||||
"timestamp": "2015-09-16T18:00:00+00:00",
|
||||
"emails_sent": 0,
|
||||
"unique_opens": 0,
|
||||
"recipients_clicks": 0
|
||||
}
|
||||
],
|
||||
"share_report": {
|
||||
"share_url": "http://usX.vip-reports.net/reports/summary?u=xxxx&id=xxxx",
|
||||
"share_password": "freddielikesjokes"
|
||||
},
|
||||
"delivery_status": {
|
||||
"enabled": false
|
||||
},
|
||||
"_links": [
|
||||
{
|
||||
"rel": "parent",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Collection.json",
|
||||
"schema": "https://api.mailchimp.com/schema/3.0/CollectionLinks/Reports.json"
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Instance.json"
|
||||
},
|
||||
{
|
||||
"rel": "campaign",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/campaigns/42694e9e57",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Campaigns/Instance.json"
|
||||
},
|
||||
{
|
||||
"rel": "sub-reports",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/sub-reports",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Sub/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "abuse-reports",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/abuse-reports",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Abuse/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "advice",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/advice",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Advice/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "click-details",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/click-details",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/ClickDetails/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "domain-performance",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/domain-performance",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/DomainPerformance/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "eepurl",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/eepurl",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Eepurl/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "email-activity",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/email-activity",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/EmailActivity/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "locations",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/locations",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Locations/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "sent-to",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/sent-to",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/SentTo/Collection.json"
|
||||
},
|
||||
{
|
||||
"rel": "unsubscribed",
|
||||
"href": "https://usX.api.mailchimp.com/3.0/reports/42694e9e57/unsubscribed",
|
||||
"method": "GET",
|
||||
"targetSchema": "https://api.mailchimp.com/schema/3.0/Reports/Unsubs/Collection.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
var sampleError = `
|
||||
{
|
||||
"type": "http://developer.mailchimp.com/documentation/mailchimp/guides/error-glossary/",
|
||||
"title": "API Key Invalid",
|
||||
"status": 401,
|
||||
"detail": "Your API key may be invalid, or you've attempted to access the wrong datacenter.",
|
||||
"instance": ""
|
||||
}
|
||||
`
|
||||
184
plugins/inputs/memcached/memcached.go
Normal file
184
plugins/inputs/memcached/memcached.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package memcached
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
// Memcached is a memcached plugin
|
||||
type Memcached struct {
|
||||
Servers []string
|
||||
UnixSockets []string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of address to gather stats about. Specify an ip on hostname
|
||||
# with optional port. ie localhost, 10.0.0.1:11211, etc.
|
||||
#
|
||||
# If no servers are specified, then localhost is used as the host.
|
||||
servers = ["localhost:11211"]
|
||||
# unix_sockets = ["/var/run/memcached.sock"]
|
||||
`
|
||||
|
||||
var defaultTimeout = 5 * time.Second
|
||||
|
||||
// The list of metrics that should be sent
|
||||
var sendMetrics = []string{
|
||||
"get_hits",
|
||||
"get_misses",
|
||||
"evictions",
|
||||
"limit_maxbytes",
|
||||
"bytes",
|
||||
"uptime",
|
||||
"curr_items",
|
||||
"total_items",
|
||||
"curr_connections",
|
||||
"total_connections",
|
||||
"connection_structures",
|
||||
"cmd_get",
|
||||
"cmd_set",
|
||||
"delete_hits",
|
||||
"delete_misses",
|
||||
"incr_hits",
|
||||
"incr_misses",
|
||||
"decr_hits",
|
||||
"decr_misses",
|
||||
"cas_hits",
|
||||
"cas_misses",
|
||||
"evictions",
|
||||
"bytes_read",
|
||||
"bytes_written",
|
||||
"threads",
|
||||
"conn_yields",
|
||||
}
|
||||
|
||||
// SampleConfig returns sample configuration message
|
||||
func (m *Memcached) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
// Description returns description of Memcached plugin
|
||||
func (m *Memcached) Description() string {
|
||||
return "Read metrics from one or many memcached servers"
|
||||
}
|
||||
|
||||
// Gather reads stats from all configured servers accumulates stats
|
||||
func (m *Memcached) Gather(acc inputs.Accumulator) error {
|
||||
if len(m.Servers) == 0 && len(m.UnixSockets) == 0 {
|
||||
return m.gatherServer(":11211", false, acc)
|
||||
}
|
||||
|
||||
for _, serverAddress := range m.Servers {
|
||||
if err := m.gatherServer(serverAddress, false, acc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, unixAddress := range m.UnixSockets {
|
||||
if err := m.gatherServer(unixAddress, true, acc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memcached) gatherServer(
|
||||
address string,
|
||||
unix bool,
|
||||
acc inputs.Accumulator,
|
||||
) error {
|
||||
var conn net.Conn
|
||||
if unix {
|
||||
conn, err := net.DialTimeout("unix", address, defaultTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
} else {
|
||||
_, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
address = address + ":11211"
|
||||
}
|
||||
|
||||
conn, err = net.DialTimeout("tcp", address, defaultTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
// Extend connection
|
||||
conn.SetDeadline(time.Now().Add(defaultTimeout))
|
||||
|
||||
// Read and write buffer
|
||||
rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
||||
|
||||
// Send command
|
||||
if _, err := fmt.Fprint(rw, "stats\r\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
values, err := parseResponse(rw.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add server address as a tag
|
||||
tags := map[string]string{"server": address}
|
||||
|
||||
// Process values
|
||||
fields := make(map[string]interface{})
|
||||
for _, key := range sendMetrics {
|
||||
if value, ok := values[key]; ok {
|
||||
// Mostly it is the number
|
||||
if iValue, errParse := strconv.ParseInt(value, 10, 64); errParse == nil {
|
||||
fields[key] = iValue
|
||||
} else {
|
||||
fields[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
acc.AddFields("memcached", fields, tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseResponse(r *bufio.Reader) (map[string]string, error) {
|
||||
values := make(map[string]string)
|
||||
|
||||
for {
|
||||
// Read line
|
||||
line, _, errRead := r.ReadLine()
|
||||
if errRead != nil {
|
||||
return values, errRead
|
||||
}
|
||||
// Done
|
||||
if bytes.Equal(line, []byte("END")) {
|
||||
break
|
||||
}
|
||||
// Read values
|
||||
s := bytes.SplitN(line, []byte(" "), 3)
|
||||
if len(s) != 3 || !bytes.Equal(s[0], []byte("STAT")) {
|
||||
return values, fmt.Errorf("unexpected line in stats response: %q", line)
|
||||
}
|
||||
|
||||
// Save values
|
||||
values[string(s[1])] = string(s[2])
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("memcached", func() inputs.Input {
|
||||
return &Memcached{}
|
||||
})
|
||||
}
|
||||
160
plugins/inputs/memcached/memcached_test.go
Normal file
160
plugins/inputs/memcached/memcached_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package memcached
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMemcachedGeneratesMetrics(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
m := &Memcached{
|
||||
Servers: []string{testutil.GetLocalHost()},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := m.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
intMetrics := []string{"get_hits", "get_misses", "evictions",
|
||||
"limit_maxbytes", "bytes", "uptime", "curr_items", "total_items",
|
||||
"curr_connections", "total_connections", "connection_structures", "cmd_get",
|
||||
"cmd_set", "delete_hits", "delete_misses", "incr_hits", "incr_misses",
|
||||
"decr_hits", "decr_misses", "cas_hits", "cas_misses", "evictions",
|
||||
"bytes_read", "bytes_written", "threads", "conn_yields"}
|
||||
|
||||
for _, metric := range intMetrics {
|
||||
assert.True(t, acc.HasIntField("memcached", metric), metric)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemcachedParseMetrics(t *testing.T) {
|
||||
r := bufio.NewReader(strings.NewReader(memcachedStats))
|
||||
values, err := parseResponse(r)
|
||||
require.NoError(t, err, "Error parsing memcached response")
|
||||
|
||||
tests := []struct {
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{"pid", "23235"},
|
||||
{"uptime", "194"},
|
||||
{"time", "1449174679"},
|
||||
{"version", "1.4.14 (Ubuntu)"},
|
||||
{"libevent", "2.0.21-stable"},
|
||||
{"pointer_size", "64"},
|
||||
{"rusage_user", "0.000000"},
|
||||
{"rusage_system", "0.007566"},
|
||||
{"curr_connections", "5"},
|
||||
{"total_connections", "6"},
|
||||
{"connection_structures", "6"},
|
||||
{"reserved_fds", "20"},
|
||||
{"cmd_get", "0"},
|
||||
{"cmd_set", "0"},
|
||||
{"cmd_flush", "0"},
|
||||
{"cmd_touch", "0"},
|
||||
{"get_hits", "0"},
|
||||
{"get_misses", "0"},
|
||||
{"delete_misses", "0"},
|
||||
{"delete_hits", "0"},
|
||||
{"incr_misses", "0"},
|
||||
{"incr_hits", "0"},
|
||||
{"decr_misses", "0"},
|
||||
{"decr_hits", "0"},
|
||||
{"cas_misses", "0"},
|
||||
{"cas_hits", "0"},
|
||||
{"cas_badval", "0"},
|
||||
{"touch_hits", "0"},
|
||||
{"touch_misses", "0"},
|
||||
{"auth_cmds", "0"},
|
||||
{"auth_errors", "0"},
|
||||
{"bytes_read", "7"},
|
||||
{"bytes_written", "0"},
|
||||
{"limit_maxbytes", "67108864"},
|
||||
{"accepting_conns", "1"},
|
||||
{"listen_disabled_num", "0"},
|
||||
{"threads", "4"},
|
||||
{"conn_yields", "0"},
|
||||
{"hash_power_level", "16"},
|
||||
{"hash_bytes", "524288"},
|
||||
{"hash_is_expanding", "0"},
|
||||
{"expired_unfetched", "0"},
|
||||
{"evicted_unfetched", "0"},
|
||||
{"bytes", "0"},
|
||||
{"curr_items", "0"},
|
||||
{"total_items", "0"},
|
||||
{"evictions", "0"},
|
||||
{"reclaimed", "0"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
value, ok := values[test.key]
|
||||
if !ok {
|
||||
t.Errorf("Did not find key for metric %s in values", test.key)
|
||||
continue
|
||||
}
|
||||
if value != test.value {
|
||||
t.Errorf("Metric: %s, Expected: %s, actual: %s",
|
||||
test.key, test.value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var memcachedStats = `STAT pid 23235
|
||||
STAT uptime 194
|
||||
STAT time 1449174679
|
||||
STAT version 1.4.14 (Ubuntu)
|
||||
STAT libevent 2.0.21-stable
|
||||
STAT pointer_size 64
|
||||
STAT rusage_user 0.000000
|
||||
STAT rusage_system 0.007566
|
||||
STAT curr_connections 5
|
||||
STAT total_connections 6
|
||||
STAT connection_structures 6
|
||||
STAT reserved_fds 20
|
||||
STAT cmd_get 0
|
||||
STAT cmd_set 0
|
||||
STAT cmd_flush 0
|
||||
STAT cmd_touch 0
|
||||
STAT get_hits 0
|
||||
STAT get_misses 0
|
||||
STAT delete_misses 0
|
||||
STAT delete_hits 0
|
||||
STAT incr_misses 0
|
||||
STAT incr_hits 0
|
||||
STAT decr_misses 0
|
||||
STAT decr_hits 0
|
||||
STAT cas_misses 0
|
||||
STAT cas_hits 0
|
||||
STAT cas_badval 0
|
||||
STAT touch_hits 0
|
||||
STAT touch_misses 0
|
||||
STAT auth_cmds 0
|
||||
STAT auth_errors 0
|
||||
STAT bytes_read 7
|
||||
STAT bytes_written 0
|
||||
STAT limit_maxbytes 67108864
|
||||
STAT accepting_conns 1
|
||||
STAT listen_disabled_num 0
|
||||
STAT threads 4
|
||||
STAT conn_yields 0
|
||||
STAT hash_power_level 16
|
||||
STAT hash_bytes 524288
|
||||
STAT hash_is_expanding 0
|
||||
STAT expired_unfetched 0
|
||||
STAT evicted_unfetched 0
|
||||
STAT bytes 0
|
||||
STAT curr_items 0
|
||||
STAT total_items 0
|
||||
STAT evictions 0
|
||||
STAT reclaimed 0
|
||||
END
|
||||
`
|
||||
15
plugins/inputs/mock_Plugin.go
Normal file
15
plugins/inputs/mock_Plugin.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package inputs
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
type MockPlugin struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockPlugin) Gather(_a0 Accumulator) error {
|
||||
ret := m.Called(_a0)
|
||||
|
||||
r0 := ret.Error(0)
|
||||
|
||||
return r0
|
||||
}
|
||||
146
plugins/inputs/mongodb/mongodb.go
Normal file
146
plugins/inputs/mongodb/mongodb.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
"gopkg.in/mgo.v2"
|
||||
)
|
||||
|
||||
type MongoDB struct {
|
||||
Servers []string
|
||||
Ssl Ssl
|
||||
mongos map[string]*Server
|
||||
}
|
||||
|
||||
type Ssl struct {
|
||||
Enabled bool
|
||||
CaCerts []string `toml:"cacerts"`
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of URI to gather stats about. Specify an ip or hostname
|
||||
# with optional port add password. ie mongodb://user:auth_key@10.10.3.30:27017,
|
||||
# mongodb://10.10.3.33:18832, 10.0.0.1:10000, etc.
|
||||
#
|
||||
# If no servers are specified, then 127.0.0.1 is used as the host and 27107 as the port.
|
||||
servers = ["127.0.0.1:27017"]
|
||||
`
|
||||
|
||||
func (m *MongoDB) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (*MongoDB) Description() string {
|
||||
return "Read metrics from one or many MongoDB servers"
|
||||
}
|
||||
|
||||
var localhost = &url.URL{Host: "127.0.0.1:27017"}
|
||||
|
||||
// Reads stats from all configured servers accumulates stats.
|
||||
// Returns one of the errors encountered while gather stats (if any).
|
||||
func (m *MongoDB) Gather(acc inputs.Accumulator) error {
|
||||
if len(m.Servers) == 0 {
|
||||
m.gatherServer(m.getMongoServer(localhost), acc)
|
||||
return nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var outerr error
|
||||
|
||||
for _, serv := range m.Servers {
|
||||
u, err := url.Parse(serv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse to address '%s': %s", serv, err)
|
||||
} else if u.Scheme == "" {
|
||||
u.Scheme = "mongodb"
|
||||
// fallback to simple string based address (i.e. "10.0.0.1:10000")
|
||||
u.Host = serv
|
||||
if u.Path == u.Host {
|
||||
u.Path = ""
|
||||
}
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
outerr = m.gatherServer(m.getMongoServer(u), acc)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
func (m *MongoDB) getMongoServer(url *url.URL) *Server {
|
||||
if _, ok := m.mongos[url.Host]; !ok {
|
||||
m.mongos[url.Host] = &Server{
|
||||
Url: url,
|
||||
}
|
||||
}
|
||||
return m.mongos[url.Host]
|
||||
}
|
||||
|
||||
func (m *MongoDB) gatherServer(server *Server, acc inputs.Accumulator) error {
|
||||
if server.Session == nil {
|
||||
var dialAddrs []string
|
||||
if server.Url.User != nil {
|
||||
dialAddrs = []string{server.Url.String()}
|
||||
} else {
|
||||
dialAddrs = []string{server.Url.Host}
|
||||
}
|
||||
dialInfo, err := mgo.ParseURL(dialAddrs[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse URL (%s), %s\n",
|
||||
dialAddrs[0], err.Error())
|
||||
}
|
||||
dialInfo.Direct = true
|
||||
dialInfo.Timeout = time.Duration(10) * time.Second
|
||||
|
||||
if m.Ssl.Enabled {
|
||||
tlsConfig := &tls.Config{}
|
||||
if len(m.Ssl.CaCerts) > 0 {
|
||||
roots := x509.NewCertPool()
|
||||
for _, caCert := range m.Ssl.CaCerts {
|
||||
ok := roots.AppendCertsFromPEM([]byte(caCert))
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to parse root certificate")
|
||||
}
|
||||
}
|
||||
tlsConfig.RootCAs = roots
|
||||
} else {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
dialInfo.DialServer = func(addr *mgo.ServerAddr) (net.Conn, error) {
|
||||
conn, err := tls.Dial("tcp", addr.String(), tlsConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("error in Dial, %s\n", err.Error())
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
}
|
||||
|
||||
sess, err := mgo.DialWithInfo(dialInfo)
|
||||
if err != nil {
|
||||
fmt.Printf("error dialing over ssl, %s\n", err.Error())
|
||||
return fmt.Errorf("Unable to connect to MongoDB, %s\n", err.Error())
|
||||
}
|
||||
server.Session = sess
|
||||
}
|
||||
return server.gatherData(acc)
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("mongodb", func() inputs.Input {
|
||||
return &MongoDB{
|
||||
mongos: make(map[string]*Server),
|
||||
}
|
||||
})
|
||||
}
|
||||
108
plugins/inputs/mongodb/mongodb_data.go
Normal file
108
plugins/inputs/mongodb/mongodb_data.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type MongodbData struct {
|
||||
StatLine *StatLine
|
||||
Fields map[string]interface{}
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
func NewMongodbData(statLine *StatLine, tags map[string]string) *MongodbData {
|
||||
if statLine.NodeType != "" && statLine.NodeType != "UNK" {
|
||||
tags["state"] = statLine.NodeType
|
||||
}
|
||||
return &MongodbData{
|
||||
StatLine: statLine,
|
||||
Tags: tags,
|
||||
Fields: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
var DefaultStats = map[string]string{
|
||||
"inserts_per_sec": "Insert",
|
||||
"queries_per_sec": "Query",
|
||||
"updates_per_sec": "Update",
|
||||
"deletes_per_sec": "Delete",
|
||||
"getmores_per_sec": "GetMore",
|
||||
"commands_per_sec": "Command",
|
||||
"flushes_per_sec": "Flushes",
|
||||
"vsize_megabytes": "Virtual",
|
||||
"resident_megabytes": "Resident",
|
||||
"queued_reads": "QueuedReaders",
|
||||
"queued_writes": "QueuedWriters",
|
||||
"active_reads": "ActiveReaders",
|
||||
"active_writes": "ActiveWriters",
|
||||
"net_in_bytes": "NetIn",
|
||||
"net_out_bytes": "NetOut",
|
||||
"open_connections": "NumConnections",
|
||||
}
|
||||
|
||||
var DefaultReplStats = map[string]string{
|
||||
"repl_inserts_per_sec": "InsertR",
|
||||
"repl_queries_per_sec": "QueryR",
|
||||
"repl_updates_per_sec": "UpdateR",
|
||||
"repl_deletes_per_sec": "DeleteR",
|
||||
"repl_getmores_per_sec": "GetMoreR",
|
||||
"repl_commands_per_sec": "CommandR",
|
||||
"member_status": "NodeType",
|
||||
}
|
||||
|
||||
var MmapStats = map[string]string{
|
||||
"mapped_megabytes": "Mapped",
|
||||
"non-mapped_megabytes": "NonMapped",
|
||||
"page_faults_per_sec": "Faults",
|
||||
}
|
||||
|
||||
var WiredTigerStats = map[string]string{
|
||||
"percent_cache_dirty": "CacheDirtyPercent",
|
||||
"percent_cache_used": "CacheUsedPercent",
|
||||
}
|
||||
|
||||
func (d *MongodbData) AddDefaultStats() {
|
||||
statLine := reflect.ValueOf(d.StatLine).Elem()
|
||||
d.addStat(statLine, DefaultStats)
|
||||
if d.StatLine.NodeType != "" {
|
||||
d.addStat(statLine, DefaultReplStats)
|
||||
}
|
||||
if d.StatLine.StorageEngine == "mmapv1" {
|
||||
d.addStat(statLine, MmapStats)
|
||||
} else if d.StatLine.StorageEngine == "wiredTiger" {
|
||||
for key, value := range WiredTigerStats {
|
||||
val := statLine.FieldByName(value).Interface()
|
||||
percentVal := fmt.Sprintf("%.1f", val.(float64)*100)
|
||||
floatVal, _ := strconv.ParseFloat(percentVal, 64)
|
||||
d.add(key, floatVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MongodbData) addStat(
|
||||
statLine reflect.Value,
|
||||
stats map[string]string,
|
||||
) {
|
||||
for key, value := range stats {
|
||||
val := statLine.FieldByName(value).Interface()
|
||||
d.add(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MongodbData) add(key string, val interface{}) {
|
||||
d.Fields[key] = val
|
||||
}
|
||||
|
||||
func (d *MongodbData) flush(acc inputs.Accumulator) {
|
||||
acc.AddFields(
|
||||
"mongodb",
|
||||
d.Fields,
|
||||
d.Tags,
|
||||
d.StatLine.Time,
|
||||
)
|
||||
d.Fields = make(map[string]interface{})
|
||||
}
|
||||
133
plugins/inputs/mongodb/mongodb_data_test.go
Normal file
133
plugins/inputs/mongodb/mongodb_data_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var tags = make(map[string]string)
|
||||
|
||||
func TestAddNonReplStats(t *testing.T) {
|
||||
d := NewMongodbData(
|
||||
&StatLine{
|
||||
StorageEngine: "",
|
||||
Time: time.Now(),
|
||||
Insert: 0,
|
||||
Query: 0,
|
||||
Update: 0,
|
||||
Delete: 0,
|
||||
GetMore: 0,
|
||||
Command: 0,
|
||||
Flushes: 0,
|
||||
Virtual: 0,
|
||||
Resident: 0,
|
||||
QueuedReaders: 0,
|
||||
QueuedWriters: 0,
|
||||
ActiveReaders: 0,
|
||||
ActiveWriters: 0,
|
||||
NetIn: 0,
|
||||
NetOut: 0,
|
||||
NumConnections: 0,
|
||||
},
|
||||
tags,
|
||||
)
|
||||
var acc testutil.Accumulator
|
||||
|
||||
d.AddDefaultStats()
|
||||
d.flush(&acc)
|
||||
|
||||
for key, _ := range DefaultStats {
|
||||
assert.True(t, acc.HasIntField("mongodb", key))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddReplStats(t *testing.T) {
|
||||
d := NewMongodbData(
|
||||
&StatLine{
|
||||
StorageEngine: "mmapv1",
|
||||
Mapped: 0,
|
||||
NonMapped: 0,
|
||||
Faults: 0,
|
||||
},
|
||||
tags,
|
||||
)
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
d.AddDefaultStats()
|
||||
d.flush(&acc)
|
||||
|
||||
for key, _ := range MmapStats {
|
||||
assert.True(t, acc.HasIntField("mongodb", key))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddWiredTigerStats(t *testing.T) {
|
||||
d := NewMongodbData(
|
||||
&StatLine{
|
||||
StorageEngine: "wiredTiger",
|
||||
CacheDirtyPercent: 0,
|
||||
CacheUsedPercent: 0,
|
||||
},
|
||||
tags,
|
||||
)
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
d.AddDefaultStats()
|
||||
d.flush(&acc)
|
||||
|
||||
for key, _ := range WiredTigerStats {
|
||||
assert.True(t, acc.HasFloatField("mongodb", key))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateTag(t *testing.T) {
|
||||
d := NewMongodbData(
|
||||
&StatLine{
|
||||
StorageEngine: "",
|
||||
Time: time.Now(),
|
||||
Insert: 0,
|
||||
Query: 0,
|
||||
NodeType: "PRI",
|
||||
},
|
||||
tags,
|
||||
)
|
||||
|
||||
stateTags := make(map[string]string)
|
||||
stateTags["state"] = "PRI"
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
d.AddDefaultStats()
|
||||
d.flush(&acc)
|
||||
fields := map[string]interface{}{
|
||||
"active_reads": int64(0),
|
||||
"active_writes": int64(0),
|
||||
"commands_per_sec": int64(0),
|
||||
"deletes_per_sec": int64(0),
|
||||
"flushes_per_sec": int64(0),
|
||||
"getmores_per_sec": int64(0),
|
||||
"inserts_per_sec": int64(0),
|
||||
"member_status": "PRI",
|
||||
"net_in_bytes": int64(0),
|
||||
"net_out_bytes": int64(0),
|
||||
"open_connections": int64(0),
|
||||
"queries_per_sec": int64(0),
|
||||
"queued_reads": int64(0),
|
||||
"queued_writes": int64(0),
|
||||
"repl_commands_per_sec": int64(0),
|
||||
"repl_deletes_per_sec": int64(0),
|
||||
"repl_getmores_per_sec": int64(0),
|
||||
"repl_inserts_per_sec": int64(0),
|
||||
"repl_queries_per_sec": int64(0),
|
||||
"repl_updates_per_sec": int64(0),
|
||||
"resident_megabytes": int64(0),
|
||||
"updates_per_sec": int64(0),
|
||||
"vsize_megabytes": int64(0),
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "mongodb", fields, stateTags)
|
||||
}
|
||||
51
plugins/inputs/mongodb/mongodb_server.go
Normal file
51
plugins/inputs/mongodb/mongodb_server.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Url *url.URL
|
||||
Session *mgo.Session
|
||||
lastResult *ServerStatus
|
||||
}
|
||||
|
||||
func (s *Server) getDefaultTags() map[string]string {
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = s.Url.Host
|
||||
return tags
|
||||
}
|
||||
|
||||
func (s *Server) gatherData(acc inputs.Accumulator) error {
|
||||
s.Session.SetMode(mgo.Eventual, true)
|
||||
s.Session.SetSocketTimeout(0)
|
||||
result := &ServerStatus{}
|
||||
err := s.Session.DB("admin").Run(bson.D{{"serverStatus", 1}, {"recordStats", 0}}, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
s.lastResult = result
|
||||
}()
|
||||
|
||||
result.SampleTime = time.Now()
|
||||
if s.lastResult != nil && result != nil {
|
||||
duration := result.SampleTime.Sub(s.lastResult.SampleTime)
|
||||
durationInSeconds := int64(duration.Seconds())
|
||||
if durationInSeconds == 0 {
|
||||
durationInSeconds = 1
|
||||
}
|
||||
data := NewMongodbData(
|
||||
NewStatLine(*s.lastResult, *result, s.Url.Host, true, durationInSeconds),
|
||||
s.getDefaultTags(),
|
||||
)
|
||||
data.AddDefaultStats()
|
||||
data.flush(acc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
43
plugins/inputs/mongodb/mongodb_server_test.go
Normal file
43
plugins/inputs/mongodb/mongodb_server_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// +build integration
|
||||
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetDefaultTags(t *testing.T) {
|
||||
var tagTests = []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"hostname", server.Url.Host},
|
||||
}
|
||||
defaultTags := server.getDefaultTags()
|
||||
for _, tt := range tagTests {
|
||||
if defaultTags[tt.in] != tt.out {
|
||||
t.Errorf("expected %q, got %q", tt.out, defaultTags[tt.in])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddDefaultStats(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := server.gatherData(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(time.Duration(1) * time.Second)
|
||||
// need to call this twice so it can perform the diff
|
||||
err = server.gatherData(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
for key, _ := range DefaultStats {
|
||||
assert.True(t, acc.HasIntValue(key))
|
||||
}
|
||||
}
|
||||
71
plugins/inputs/mongodb/mongodb_test.go
Normal file
71
plugins/inputs/mongodb/mongodb_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// +build integration
|
||||
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/mgo.v2"
|
||||
)
|
||||
|
||||
var connect_url string
|
||||
var server *Server
|
||||
|
||||
func init() {
|
||||
connect_url = os.Getenv("MONGODB_URL")
|
||||
if connect_url == "" {
|
||||
connect_url = "127.0.0.1:27017"
|
||||
server = &Server{Url: &url.URL{Host: connect_url}}
|
||||
} else {
|
||||
full_url, err := url.Parse(connect_url)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to parse URL (%s), %s\n", full_url, err.Error())
|
||||
}
|
||||
server = &Server{Url: full_url}
|
||||
}
|
||||
}
|
||||
|
||||
func testSetup(m *testing.M) {
|
||||
var err error
|
||||
var dialAddrs []string
|
||||
if server.Url.User != nil {
|
||||
dialAddrs = []string{server.Url.String()}
|
||||
} else {
|
||||
dialAddrs = []string{server.Url.Host}
|
||||
}
|
||||
dialInfo, err := mgo.ParseURL(dialAddrs[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to parse URL (%s), %s\n", dialAddrs[0], err.Error())
|
||||
}
|
||||
dialInfo.Direct = true
|
||||
dialInfo.Timeout = time.Duration(10) * time.Second
|
||||
sess, err := mgo.DialWithInfo(dialInfo)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to connect to MongoDB, %s\n", err.Error())
|
||||
}
|
||||
server.Session = sess
|
||||
server.Session, _ = mgo.Dial(server.Url.Host)
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func testTeardown(m *testing.M) {
|
||||
server.Session.Close()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// seed randomness for use with tests
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
testSetup(m)
|
||||
res := m.Run()
|
||||
testTeardown(m)
|
||||
|
||||
os.Exit(res)
|
||||
}
|
||||
591
plugins/inputs/mongodb/mongostat.go
Normal file
591
plugins/inputs/mongodb/mongostat.go
Normal file
@@ -0,0 +1,591 @@
|
||||
/***
|
||||
The code contained here came from https://github.com/mongodb/mongo-tools/blob/master/mongostat/stat_types.go
|
||||
and contains modifications so that no other dependency from that project is needed. Other modifications included
|
||||
removing uneccessary code specific to formatting the output and determine the current state of the database. It
|
||||
is licensed under Apache Version 2.0, http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
***/
|
||||
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
MongosProcess = "mongos"
|
||||
)
|
||||
|
||||
// Flags to determine cases when to activate/deactivate columns for output.
|
||||
const (
|
||||
Always = 1 << iota // always activate the column
|
||||
Discover // only active when mongostat is in discover mode
|
||||
Repl // only active if one of the nodes being monitored is in a replset
|
||||
Locks // only active if node is capable of calculating lock info
|
||||
AllOnly // only active if mongostat was run with --all option
|
||||
MMAPOnly // only active if node has mmap-specific fields
|
||||
WTOnly // only active if node has wiredtiger-specific fields
|
||||
)
|
||||
|
||||
type ServerStatus struct {
|
||||
SampleTime time.Time `bson:""`
|
||||
Host string `bson:"host"`
|
||||
Version string `bson:"version"`
|
||||
Process string `bson:"process"`
|
||||
Pid int64 `bson:"pid"`
|
||||
Uptime int64 `bson:"uptime"`
|
||||
UptimeMillis int64 `bson:"uptimeMillis"`
|
||||
UptimeEstimate int64 `bson:"uptimeEstimate"`
|
||||
LocalTime time.Time `bson:"localTime"`
|
||||
Asserts map[string]int64 `bson:"asserts"`
|
||||
BackgroundFlushing *FlushStats `bson:"backgroundFlushing"`
|
||||
ExtraInfo *ExtraInfo `bson:"extra_info"`
|
||||
Connections *ConnectionStats `bson:"connections"`
|
||||
Dur *DurStats `bson:"dur"`
|
||||
GlobalLock *GlobalLockStats `bson:"globalLock"`
|
||||
Locks map[string]LockStats `bson:"locks,omitempty"`
|
||||
Network *NetworkStats `bson:"network"`
|
||||
Opcounters *OpcountStats `bson:"opcounters"`
|
||||
OpcountersRepl *OpcountStats `bson:"opcountersRepl"`
|
||||
RecordStats *DBRecordStats `bson:"recordStats"`
|
||||
Mem *MemStats `bson:"mem"`
|
||||
Repl *ReplStatus `bson:"repl"`
|
||||
ShardCursorType map[string]interface{} `bson:"shardCursorType"`
|
||||
StorageEngine map[string]string `bson:"storageEngine"`
|
||||
WiredTiger *WiredTiger `bson:"wiredTiger"`
|
||||
}
|
||||
|
||||
// WiredTiger stores information related to the WiredTiger storage engine.
|
||||
type WiredTiger struct {
|
||||
Transaction TransactionStats `bson:"transaction"`
|
||||
Concurrent ConcurrentTransactions `bson:"concurrentTransactions"`
|
||||
Cache CacheStats `bson:"cache"`
|
||||
}
|
||||
|
||||
type ConcurrentTransactions struct {
|
||||
Write ConcurrentTransStats `bson:"write"`
|
||||
Read ConcurrentTransStats `bson:"read"`
|
||||
}
|
||||
|
||||
type ConcurrentTransStats struct {
|
||||
Out int64 `bson:"out"`
|
||||
}
|
||||
|
||||
// CacheStats stores cache statistics for WiredTiger.
|
||||
type CacheStats struct {
|
||||
TrackedDirtyBytes int64 `bson:"tracked dirty bytes in the cache"`
|
||||
CurrentCachedBytes int64 `bson:"bytes currently in the cache"`
|
||||
MaxBytesConfigured int64 `bson:"maximum bytes configured"`
|
||||
}
|
||||
|
||||
// TransactionStats stores transaction checkpoints in WiredTiger.
|
||||
type TransactionStats struct {
|
||||
TransCheckpoints int64 `bson:"transaction checkpoints"`
|
||||
}
|
||||
|
||||
// ReplStatus stores data related to replica sets.
|
||||
type ReplStatus struct {
|
||||
SetName interface{} `bson:"setName"`
|
||||
IsMaster interface{} `bson:"ismaster"`
|
||||
Secondary interface{} `bson:"secondary"`
|
||||
IsReplicaSet interface{} `bson:"isreplicaset"`
|
||||
ArbiterOnly interface{} `bson:"arbiterOnly"`
|
||||
Hosts []string `bson:"hosts"`
|
||||
Passives []string `bson:"passives"`
|
||||
Me string `bson:"me"`
|
||||
}
|
||||
|
||||
// DBRecordStats stores data related to memory operations across databases.
|
||||
type DBRecordStats struct {
|
||||
AccessesNotInMemory int64 `bson:"accessesNotInMemory"`
|
||||
PageFaultExceptionsThrown int64 `bson:"pageFaultExceptionsThrown"`
|
||||
DBRecordAccesses map[string]RecordAccesses `bson:",inline"`
|
||||
}
|
||||
|
||||
// RecordAccesses stores data related to memory operations scoped to a database.
|
||||
type RecordAccesses struct {
|
||||
AccessesNotInMemory int64 `bson:"accessesNotInMemory"`
|
||||
PageFaultExceptionsThrown int64 `bson:"pageFaultExceptionsThrown"`
|
||||
}
|
||||
|
||||
// MemStats stores data related to memory statistics.
|
||||
type MemStats struct {
|
||||
Bits int64 `bson:"bits"`
|
||||
Resident int64 `bson:"resident"`
|
||||
Virtual int64 `bson:"virtual"`
|
||||
Supported interface{} `bson:"supported"`
|
||||
Mapped int64 `bson:"mapped"`
|
||||
MappedWithJournal int64 `bson:"mappedWithJournal"`
|
||||
}
|
||||
|
||||
// FlushStats stores information about memory flushes.
|
||||
type FlushStats struct {
|
||||
Flushes int64 `bson:"flushes"`
|
||||
TotalMs int64 `bson:"total_ms"`
|
||||
AverageMs float64 `bson:"average_ms"`
|
||||
LastMs int64 `bson:"last_ms"`
|
||||
LastFinished time.Time `bson:"last_finished"`
|
||||
}
|
||||
|
||||
// ConnectionStats stores information related to incoming database connections.
|
||||
type ConnectionStats struct {
|
||||
Current int64 `bson:"current"`
|
||||
Available int64 `bson:"available"`
|
||||
TotalCreated int64 `bson:"totalCreated"`
|
||||
}
|
||||
|
||||
// DurTiming stores information related to journaling.
|
||||
type DurTiming struct {
|
||||
Dt int64 `bson:"dt"`
|
||||
PrepLogBuffer int64 `bson:"prepLogBuffer"`
|
||||
WriteToJournal int64 `bson:"writeToJournal"`
|
||||
WriteToDataFiles int64 `bson:"writeToDataFiles"`
|
||||
RemapPrivateView int64 `bson:"remapPrivateView"`
|
||||
}
|
||||
|
||||
// DurStats stores information related to journaling statistics.
|
||||
type DurStats struct {
|
||||
Commits int64 `bson:"commits"`
|
||||
JournaledMB int64 `bson:"journaledMB"`
|
||||
WriteToDataFilesMB int64 `bson:"writeToDataFilesMB"`
|
||||
Compression int64 `bson:"compression"`
|
||||
CommitsInWriteLock int64 `bson:"commitsInWriteLock"`
|
||||
EarlyCommits int64 `bson:"earlyCommits"`
|
||||
TimeMs DurTiming
|
||||
}
|
||||
|
||||
// QueueStats stores the number of queued read/write operations.
|
||||
type QueueStats struct {
|
||||
Total int64 `bson:"total"`
|
||||
Readers int64 `bson:"readers"`
|
||||
Writers int64 `bson:"writers"`
|
||||
}
|
||||
|
||||
// ClientStats stores the number of active read/write operations.
|
||||
type ClientStats struct {
|
||||
Total int64 `bson:"total"`
|
||||
Readers int64 `bson:"readers"`
|
||||
Writers int64 `bson:"writers"`
|
||||
}
|
||||
|
||||
// GlobalLockStats stores information related locks in the MMAP storage engine.
|
||||
type GlobalLockStats struct {
|
||||
TotalTime int64 `bson:"totalTime"`
|
||||
LockTime int64 `bson:"lockTime"`
|
||||
CurrentQueue *QueueStats `bson:"currentQueue"`
|
||||
ActiveClients *ClientStats `bson:"activeClients"`
|
||||
}
|
||||
|
||||
// NetworkStats stores information related to network traffic.
|
||||
type NetworkStats struct {
|
||||
BytesIn int64 `bson:"bytesIn"`
|
||||
BytesOut int64 `bson:"bytesOut"`
|
||||
NumRequests int64 `bson:"numRequests"`
|
||||
}
|
||||
|
||||
// OpcountStats stores information related to comamnds and basic CRUD operations.
|
||||
type OpcountStats struct {
|
||||
Insert int64 `bson:"insert"`
|
||||
Query int64 `bson:"query"`
|
||||
Update int64 `bson:"update"`
|
||||
Delete int64 `bson:"delete"`
|
||||
GetMore int64 `bson:"getmore"`
|
||||
Command int64 `bson:"command"`
|
||||
}
|
||||
|
||||
// ReadWriteLockTimes stores time spent holding read/write locks.
|
||||
type ReadWriteLockTimes struct {
|
||||
Read int64 `bson:"R"`
|
||||
Write int64 `bson:"W"`
|
||||
ReadLower int64 `bson:"r"`
|
||||
WriteLower int64 `bson:"w"`
|
||||
}
|
||||
|
||||
// LockStats stores information related to time spent acquiring/holding locks
|
||||
// for a given database.
|
||||
type LockStats struct {
|
||||
TimeLockedMicros ReadWriteLockTimes `bson:"timeLockedMicros"`
|
||||
TimeAcquiringMicros ReadWriteLockTimes `bson:"timeAcquiringMicros"`
|
||||
|
||||
// AcquireCount and AcquireWaitCount are new fields of the lock stats only populated on 3.0 or newer.
|
||||
// Typed as a pointer so that if it is nil, mongostat can assume the field is not populated
|
||||
// with real namespace data.
|
||||
AcquireCount *ReadWriteLockTimes `bson:"acquireCount,omitempty"`
|
||||
AcquireWaitCount *ReadWriteLockTimes `bson:"acquireWaitCount,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraInfo stores additional platform specific information.
|
||||
type ExtraInfo struct {
|
||||
PageFaults *int64 `bson:"page_faults"`
|
||||
}
|
||||
|
||||
// StatHeader describes a single column for mongostat's terminal output,
|
||||
// its formatting, and in which modes it should be displayed.
|
||||
type StatHeader struct {
|
||||
// The text to appear in the column's header cell
|
||||
HeaderText string
|
||||
|
||||
// Bitmask containing flags to determine if this header is active or not
|
||||
ActivateFlags int
|
||||
}
|
||||
|
||||
// StatHeaders are the complete set of data metrics supported by mongostat.
|
||||
var StatHeaders = []StatHeader{
|
||||
{"", Always}, // placeholder for hostname column (blank header text)
|
||||
{"insert", Always},
|
||||
{"query", Always},
|
||||
{"update", Always},
|
||||
{"delete", Always},
|
||||
{"getmore", Always},
|
||||
{"command", Always},
|
||||
{"% dirty", WTOnly},
|
||||
{"% used", WTOnly},
|
||||
{"flushes", Always},
|
||||
{"mapped", MMAPOnly},
|
||||
{"vsize", Always},
|
||||
{"res", Always},
|
||||
{"non-mapped", MMAPOnly | AllOnly},
|
||||
{"faults", MMAPOnly},
|
||||
{"lr|lw %", MMAPOnly | AllOnly},
|
||||
{"lrt|lwt", MMAPOnly | AllOnly},
|
||||
{" locked db", Locks},
|
||||
{"qr|qw", Always},
|
||||
{"ar|aw", Always},
|
||||
{"netIn", Always},
|
||||
{"netOut", Always},
|
||||
{"conn", Always},
|
||||
{"set", Repl},
|
||||
{"repl", Repl},
|
||||
{"time", Always},
|
||||
}
|
||||
|
||||
// NamespacedLocks stores information on the LockStatus of namespaces.
|
||||
type NamespacedLocks map[string]LockStatus
|
||||
|
||||
// LockUsage stores information related to a namespace's lock usage.
|
||||
type LockUsage struct {
|
||||
Namespace string
|
||||
Reads int64
|
||||
Writes int64
|
||||
}
|
||||
|
||||
type lockUsages []LockUsage
|
||||
|
||||
func percentageInt64(value, outOf int64) float64 {
|
||||
if value == 0 || outOf == 0 {
|
||||
return 0
|
||||
}
|
||||
return 100 * (float64(value) / float64(outOf))
|
||||
}
|
||||
|
||||
func averageInt64(value, outOf int64) int64 {
|
||||
if value == 0 || outOf == 0 {
|
||||
return 0
|
||||
}
|
||||
return value / outOf
|
||||
}
|
||||
|
||||
func (slice lockUsages) Len() int {
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
func (slice lockUsages) Less(i, j int) bool {
|
||||
return slice[i].Reads+slice[i].Writes < slice[j].Reads+slice[j].Writes
|
||||
}
|
||||
|
||||
func (slice lockUsages) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
|
||||
// CollectionLockStatus stores a collection's lock statistics.
|
||||
type CollectionLockStatus struct {
|
||||
ReadAcquireWaitsPercentage float64
|
||||
WriteAcquireWaitsPercentage float64
|
||||
ReadAcquireTimeMicros int64
|
||||
WriteAcquireTimeMicros int64
|
||||
}
|
||||
|
||||
// LockStatus stores a database's lock statistics.
|
||||
type LockStatus struct {
|
||||
DBName string
|
||||
Percentage float64
|
||||
Global bool
|
||||
}
|
||||
|
||||
// StatLine is a wrapper for all metrics reported by mongostat for monitored hosts.
|
||||
type StatLine struct {
|
||||
Key string
|
||||
// What storage engine is being used for the node with this stat line
|
||||
StorageEngine string
|
||||
|
||||
Error error
|
||||
IsMongos bool
|
||||
Host string
|
||||
|
||||
// The time at which this StatLine was generated.
|
||||
Time time.Time
|
||||
|
||||
// The last time at which this StatLine was printed to output.
|
||||
LastPrinted time.Time
|
||||
|
||||
// Opcounter fields
|
||||
Insert, Query, Update, Delete, GetMore, Command int64
|
||||
|
||||
// Collection locks (3.0 mmap only)
|
||||
CollectionLocks *CollectionLockStatus
|
||||
|
||||
// Cache utilization (wiredtiger only)
|
||||
CacheDirtyPercent float64
|
||||
CacheUsedPercent float64
|
||||
|
||||
// Replicated Opcounter fields
|
||||
InsertR, QueryR, UpdateR, DeleteR, GetMoreR, CommandR int64
|
||||
Flushes int64
|
||||
Mapped, Virtual, Resident, NonMapped int64
|
||||
Faults int64
|
||||
HighestLocked *LockStatus
|
||||
QueuedReaders, QueuedWriters int64
|
||||
ActiveReaders, ActiveWriters int64
|
||||
NetIn, NetOut int64
|
||||
NumConnections int64
|
||||
ReplSetName string
|
||||
NodeType string
|
||||
}
|
||||
|
||||
func parseLocks(stat ServerStatus) map[string]LockUsage {
|
||||
returnVal := map[string]LockUsage{}
|
||||
for namespace, lockInfo := range stat.Locks {
|
||||
returnVal[namespace] = LockUsage{
|
||||
namespace,
|
||||
lockInfo.TimeLockedMicros.Read + lockInfo.TimeLockedMicros.ReadLower,
|
||||
lockInfo.TimeLockedMicros.Write + lockInfo.TimeLockedMicros.WriteLower,
|
||||
}
|
||||
}
|
||||
return returnVal
|
||||
}
|
||||
|
||||
func computeLockDiffs(prevLocks, curLocks map[string]LockUsage) []LockUsage {
|
||||
lockUsages := lockUsages(make([]LockUsage, 0, len(curLocks)))
|
||||
for namespace, curUsage := range curLocks {
|
||||
prevUsage, hasKey := prevLocks[namespace]
|
||||
if !hasKey {
|
||||
// This namespace didn't appear in the previous batch of lock info,
|
||||
// so we can't compute a diff for it - skip it.
|
||||
continue
|
||||
}
|
||||
// Calculate diff of lock usage for this namespace and add to the list
|
||||
lockUsages = append(lockUsages,
|
||||
LockUsage{
|
||||
namespace,
|
||||
curUsage.Reads - prevUsage.Reads,
|
||||
curUsage.Writes - prevUsage.Writes,
|
||||
})
|
||||
}
|
||||
// Sort the array in order of least to most locked
|
||||
sort.Sort(lockUsages)
|
||||
return lockUsages
|
||||
}
|
||||
|
||||
func diff(newVal, oldVal, sampleTime int64) int64 {
|
||||
d := newVal - oldVal
|
||||
if d < 0 {
|
||||
d = newVal
|
||||
}
|
||||
return d / sampleTime
|
||||
}
|
||||
|
||||
// NewStatLine constructs a StatLine object from two ServerStatus objects.
|
||||
func NewStatLine(oldStat, newStat ServerStatus, key string, all bool, sampleSecs int64) *StatLine {
|
||||
returnVal := &StatLine{
|
||||
Key: key,
|
||||
Host: newStat.Host,
|
||||
Mapped: -1,
|
||||
Virtual: -1,
|
||||
Resident: -1,
|
||||
NonMapped: -1,
|
||||
Faults: -1,
|
||||
}
|
||||
|
||||
// set the storage engine appropriately
|
||||
if newStat.StorageEngine != nil && newStat.StorageEngine["name"] != "" {
|
||||
returnVal.StorageEngine = newStat.StorageEngine["name"]
|
||||
} else {
|
||||
returnVal.StorageEngine = "mmapv1"
|
||||
}
|
||||
|
||||
if newStat.Opcounters != nil && oldStat.Opcounters != nil {
|
||||
returnVal.Insert = diff(newStat.Opcounters.Insert, oldStat.Opcounters.Insert, sampleSecs)
|
||||
returnVal.Query = diff(newStat.Opcounters.Query, oldStat.Opcounters.Query, sampleSecs)
|
||||
returnVal.Update = diff(newStat.Opcounters.Update, oldStat.Opcounters.Update, sampleSecs)
|
||||
returnVal.Delete = diff(newStat.Opcounters.Delete, oldStat.Opcounters.Delete, sampleSecs)
|
||||
returnVal.GetMore = diff(newStat.Opcounters.GetMore, oldStat.Opcounters.GetMore, sampleSecs)
|
||||
returnVal.Command = diff(newStat.Opcounters.Command, oldStat.Opcounters.Command, sampleSecs)
|
||||
}
|
||||
|
||||
if newStat.OpcountersRepl != nil && oldStat.OpcountersRepl != nil {
|
||||
returnVal.InsertR = diff(newStat.OpcountersRepl.Insert, oldStat.OpcountersRepl.Insert, sampleSecs)
|
||||
returnVal.QueryR = diff(newStat.OpcountersRepl.Query, oldStat.OpcountersRepl.Query, sampleSecs)
|
||||
returnVal.UpdateR = diff(newStat.OpcountersRepl.Update, oldStat.OpcountersRepl.Update, sampleSecs)
|
||||
returnVal.DeleteR = diff(newStat.OpcountersRepl.Delete, oldStat.OpcountersRepl.Delete, sampleSecs)
|
||||
returnVal.GetMoreR = diff(newStat.OpcountersRepl.GetMore, oldStat.OpcountersRepl.GetMore, sampleSecs)
|
||||
returnVal.CommandR = diff(newStat.OpcountersRepl.Command, oldStat.OpcountersRepl.Command, sampleSecs)
|
||||
}
|
||||
|
||||
returnVal.CacheDirtyPercent = -1
|
||||
returnVal.CacheUsedPercent = -1
|
||||
if newStat.WiredTiger != nil && oldStat.WiredTiger != nil {
|
||||
returnVal.Flushes = newStat.WiredTiger.Transaction.TransCheckpoints - oldStat.WiredTiger.Transaction.TransCheckpoints
|
||||
returnVal.CacheDirtyPercent = float64(newStat.WiredTiger.Cache.TrackedDirtyBytes) / float64(newStat.WiredTiger.Cache.MaxBytesConfigured)
|
||||
returnVal.CacheUsedPercent = float64(newStat.WiredTiger.Cache.CurrentCachedBytes) / float64(newStat.WiredTiger.Cache.MaxBytesConfigured)
|
||||
} else if newStat.BackgroundFlushing != nil && oldStat.BackgroundFlushing != nil {
|
||||
returnVal.Flushes = newStat.BackgroundFlushing.Flushes - oldStat.BackgroundFlushing.Flushes
|
||||
}
|
||||
|
||||
returnVal.Time = newStat.SampleTime
|
||||
returnVal.IsMongos =
|
||||
(newStat.ShardCursorType != nil || strings.HasPrefix(newStat.Process, MongosProcess))
|
||||
|
||||
// BEGIN code modification
|
||||
if oldStat.Mem.Supported.(bool) {
|
||||
// END code modification
|
||||
if !returnVal.IsMongos {
|
||||
returnVal.Mapped = newStat.Mem.Mapped
|
||||
}
|
||||
returnVal.Virtual = newStat.Mem.Virtual
|
||||
returnVal.Resident = newStat.Mem.Resident
|
||||
|
||||
if !returnVal.IsMongos && all {
|
||||
returnVal.NonMapped = newStat.Mem.Virtual - newStat.Mem.Mapped
|
||||
}
|
||||
}
|
||||
|
||||
if newStat.Repl != nil {
|
||||
setName, isReplSet := newStat.Repl.SetName.(string)
|
||||
if isReplSet {
|
||||
returnVal.ReplSetName = setName
|
||||
}
|
||||
// BEGIN code modification
|
||||
if newStat.Repl.IsMaster.(bool) {
|
||||
returnVal.NodeType = "PRI"
|
||||
} else if newStat.Repl.Secondary.(bool) {
|
||||
returnVal.NodeType = "SEC"
|
||||
} else {
|
||||
returnVal.NodeType = "UNK"
|
||||
}
|
||||
// END code modification
|
||||
} else if returnVal.IsMongos {
|
||||
returnVal.NodeType = "RTR"
|
||||
}
|
||||
|
||||
if oldStat.ExtraInfo != nil && newStat.ExtraInfo != nil &&
|
||||
oldStat.ExtraInfo.PageFaults != nil && newStat.ExtraInfo.PageFaults != nil {
|
||||
returnVal.Faults = diff(*(newStat.ExtraInfo.PageFaults), *(oldStat.ExtraInfo.PageFaults), sampleSecs)
|
||||
}
|
||||
if !returnVal.IsMongos && oldStat.Locks != nil {
|
||||
globalCheck, hasGlobal := oldStat.Locks["Global"]
|
||||
if hasGlobal && globalCheck.AcquireCount != nil {
|
||||
// This appears to be a 3.0+ server so the data in these fields do *not* refer to
|
||||
// actual namespaces and thus we can't compute lock %.
|
||||
returnVal.HighestLocked = nil
|
||||
|
||||
// Check if it's a 3.0+ MMAP server so we can still compute collection locks
|
||||
collectionCheck, hasCollection := oldStat.Locks["Collection"]
|
||||
if hasCollection && collectionCheck.AcquireWaitCount != nil {
|
||||
readWaitCountDiff := newStat.Locks["Collection"].AcquireWaitCount.Read - oldStat.Locks["Collection"].AcquireWaitCount.Read
|
||||
readTotalCountDiff := newStat.Locks["Collection"].AcquireCount.Read - oldStat.Locks["Collection"].AcquireCount.Read
|
||||
writeWaitCountDiff := newStat.Locks["Collection"].AcquireWaitCount.Write - oldStat.Locks["Collection"].AcquireWaitCount.Write
|
||||
writeTotalCountDiff := newStat.Locks["Collection"].AcquireCount.Write - oldStat.Locks["Collection"].AcquireCount.Write
|
||||
readAcquireTimeDiff := newStat.Locks["Collection"].TimeAcquiringMicros.Read - oldStat.Locks["Collection"].TimeAcquiringMicros.Read
|
||||
writeAcquireTimeDiff := newStat.Locks["Collection"].TimeAcquiringMicros.Write - oldStat.Locks["Collection"].TimeAcquiringMicros.Write
|
||||
returnVal.CollectionLocks = &CollectionLockStatus{
|
||||
ReadAcquireWaitsPercentage: percentageInt64(readWaitCountDiff, readTotalCountDiff),
|
||||
WriteAcquireWaitsPercentage: percentageInt64(writeWaitCountDiff, writeTotalCountDiff),
|
||||
ReadAcquireTimeMicros: averageInt64(readAcquireTimeDiff, readWaitCountDiff),
|
||||
WriteAcquireTimeMicros: averageInt64(writeAcquireTimeDiff, writeWaitCountDiff),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prevLocks := parseLocks(oldStat)
|
||||
curLocks := parseLocks(newStat)
|
||||
lockdiffs := computeLockDiffs(prevLocks, curLocks)
|
||||
if len(lockdiffs) == 0 {
|
||||
if newStat.GlobalLock != nil {
|
||||
returnVal.HighestLocked = &LockStatus{
|
||||
DBName: "",
|
||||
Percentage: percentageInt64(newStat.GlobalLock.LockTime, newStat.GlobalLock.TotalTime),
|
||||
Global: true,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get the entry with the highest lock
|
||||
highestLocked := lockdiffs[len(lockdiffs)-1]
|
||||
|
||||
var timeDiffMillis int64
|
||||
timeDiffMillis = newStat.UptimeMillis - oldStat.UptimeMillis
|
||||
|
||||
lockToReport := highestLocked.Writes
|
||||
|
||||
// if the highest locked namespace is not '.'
|
||||
if highestLocked.Namespace != "." {
|
||||
for _, namespaceLockInfo := range lockdiffs {
|
||||
if namespaceLockInfo.Namespace == "." {
|
||||
lockToReport += namespaceLockInfo.Writes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lock data is in microseconds and uptime is in milliseconds - so
|
||||
// divide by 1000 so that they units match
|
||||
lockToReport /= 1000
|
||||
|
||||
returnVal.HighestLocked = &LockStatus{
|
||||
DBName: highestLocked.Namespace,
|
||||
Percentage: percentageInt64(lockToReport, timeDiffMillis),
|
||||
Global: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
returnVal.HighestLocked = nil
|
||||
}
|
||||
|
||||
if newStat.GlobalLock != nil {
|
||||
hasWT := (newStat.WiredTiger != nil && oldStat.WiredTiger != nil)
|
||||
//If we have wiredtiger stats, use those instead
|
||||
if newStat.GlobalLock.CurrentQueue != nil {
|
||||
if hasWT {
|
||||
returnVal.QueuedReaders = newStat.GlobalLock.CurrentQueue.Readers + newStat.GlobalLock.ActiveClients.Readers - newStat.WiredTiger.Concurrent.Read.Out
|
||||
returnVal.QueuedWriters = newStat.GlobalLock.CurrentQueue.Writers + newStat.GlobalLock.ActiveClients.Writers - newStat.WiredTiger.Concurrent.Write.Out
|
||||
if returnVal.QueuedReaders < 0 {
|
||||
returnVal.QueuedReaders = 0
|
||||
}
|
||||
if returnVal.QueuedWriters < 0 {
|
||||
returnVal.QueuedWriters = 0
|
||||
}
|
||||
} else {
|
||||
returnVal.QueuedReaders = newStat.GlobalLock.CurrentQueue.Readers
|
||||
returnVal.QueuedWriters = newStat.GlobalLock.CurrentQueue.Writers
|
||||
}
|
||||
}
|
||||
|
||||
if hasWT {
|
||||
returnVal.ActiveReaders = newStat.WiredTiger.Concurrent.Read.Out
|
||||
returnVal.ActiveWriters = newStat.WiredTiger.Concurrent.Write.Out
|
||||
} else if newStat.GlobalLock.ActiveClients != nil {
|
||||
returnVal.ActiveReaders = newStat.GlobalLock.ActiveClients.Readers
|
||||
returnVal.ActiveWriters = newStat.GlobalLock.ActiveClients.Writers
|
||||
}
|
||||
}
|
||||
|
||||
if oldStat.Network != nil && newStat.Network != nil {
|
||||
returnVal.NetIn = diff(newStat.Network.BytesIn, oldStat.Network.BytesIn, sampleSecs)
|
||||
returnVal.NetOut = diff(newStat.Network.BytesOut, oldStat.Network.BytesOut, sampleSecs)
|
||||
}
|
||||
|
||||
if newStat.Connections != nil {
|
||||
returnVal.NumConnections = newStat.Connections.Current
|
||||
}
|
||||
|
||||
return returnVal
|
||||
}
|
||||
213
plugins/inputs/mysql/mysql.go
Normal file
213
plugins/inputs/mysql/mysql.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type Mysql struct {
|
||||
Servers []string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# specify servers via a url matching:
|
||||
# [username[:password]@][protocol[(address)]]/[?tls=[true|false|skip-verify]]
|
||||
# see https://github.com/go-sql-driver/mysql#dsn-data-source-name
|
||||
# e.g.
|
||||
# root:passwd@tcp(127.0.0.1:3306)/?tls=false
|
||||
# root@tcp(127.0.0.1:3306)/?tls=false
|
||||
#
|
||||
# If no servers are specified, then localhost is used as the host.
|
||||
servers = ["tcp(127.0.0.1:3306)/"]
|
||||
`
|
||||
|
||||
func (m *Mysql) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (m *Mysql) Description() string {
|
||||
return "Read metrics from one or many mysql servers"
|
||||
}
|
||||
|
||||
var localhost = ""
|
||||
|
||||
func (m *Mysql) Gather(acc inputs.Accumulator) error {
|
||||
if len(m.Servers) == 0 {
|
||||
// if we can't get stats in this case, thats fine, don't report
|
||||
// an error.
|
||||
m.gatherServer(localhost, acc)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, serv := range m.Servers {
|
||||
err := m.gatherServer(serv, acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type mapping struct {
|
||||
onServer string
|
||||
inExport string
|
||||
}
|
||||
|
||||
var mappings = []*mapping{
|
||||
{
|
||||
onServer: "Aborted_",
|
||||
inExport: "aborted_",
|
||||
},
|
||||
{
|
||||
onServer: "Bytes_",
|
||||
inExport: "bytes_",
|
||||
},
|
||||
{
|
||||
onServer: "Com_",
|
||||
inExport: "commands_",
|
||||
},
|
||||
{
|
||||
onServer: "Created_",
|
||||
inExport: "created_",
|
||||
},
|
||||
{
|
||||
onServer: "Handler_",
|
||||
inExport: "handler_",
|
||||
},
|
||||
{
|
||||
onServer: "Innodb_",
|
||||
inExport: "innodb_",
|
||||
},
|
||||
{
|
||||
onServer: "Key_",
|
||||
inExport: "key_",
|
||||
},
|
||||
{
|
||||
onServer: "Open_",
|
||||
inExport: "open_",
|
||||
},
|
||||
{
|
||||
onServer: "Opened_",
|
||||
inExport: "opened_",
|
||||
},
|
||||
{
|
||||
onServer: "Qcache_",
|
||||
inExport: "qcache_",
|
||||
},
|
||||
{
|
||||
onServer: "Table_",
|
||||
inExport: "table_",
|
||||
},
|
||||
{
|
||||
onServer: "Tokudb_",
|
||||
inExport: "tokudb_",
|
||||
},
|
||||
{
|
||||
onServer: "Threads_",
|
||||
inExport: "threads_",
|
||||
},
|
||||
}
|
||||
|
||||
func (m *Mysql) gatherServer(serv string, acc inputs.Accumulator) error {
|
||||
// If user forgot the '/', add it
|
||||
if strings.HasSuffix(serv, ")") {
|
||||
serv = serv + "/"
|
||||
} else if serv == "localhost" {
|
||||
serv = ""
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", serv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`SHOW /*!50002 GLOBAL */ STATUS`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var servtag string
|
||||
servtag, err = parseDSN(serv)
|
||||
if err != nil {
|
||||
servtag = "localhost"
|
||||
}
|
||||
tags := map[string]string{"server": servtag}
|
||||
fields := make(map[string]interface{})
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var val interface{}
|
||||
|
||||
err = rows.Scan(&name, &val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var found bool
|
||||
|
||||
for _, mapped := range mappings {
|
||||
if strings.HasPrefix(name, mapped.onServer) {
|
||||
i, _ := strconv.Atoi(string(val.([]byte)))
|
||||
fields[mapped.inExport+name[len(mapped.onServer):]] = i
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "Queries":
|
||||
i, err := strconv.ParseInt(string(val.([]byte)), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields["queries"] = i
|
||||
case "Slow_queries":
|
||||
i, err := strconv.ParseInt(string(val.([]byte)), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields["slow_queries"] = i
|
||||
}
|
||||
}
|
||||
acc.AddFields("mysql", fields, tags)
|
||||
|
||||
conn_rows, err := db.Query("SELECT user, sum(1) FROM INFORMATION_SCHEMA.PROCESSLIST GROUP BY user")
|
||||
|
||||
for conn_rows.Next() {
|
||||
var user string
|
||||
var connections int64
|
||||
|
||||
err = conn_rows.Scan(&user, &connections)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags := map[string]string{"server": servtag, "user": user}
|
||||
fields := make(map[string]interface{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fields["connections"] = connections
|
||||
acc.AddFields("mysql_users", fields, tags)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("mysql", func() inputs.Input {
|
||||
return &Mysql{}
|
||||
})
|
||||
}
|
||||
86
plugins/inputs/mysql/mysql_test.go
Normal file
86
plugins/inputs/mysql/mysql_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMysqlDefaultsToLocal(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
m := &Mysql{
|
||||
Servers: []string{fmt.Sprintf("root@tcp(%s:3306)/", testutil.GetLocalHost())},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := m.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, acc.HasMeasurement("mysql"))
|
||||
}
|
||||
|
||||
func TestMysqlParseDSN(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
output string
|
||||
}{
|
||||
{
|
||||
"",
|
||||
"127.0.0.1:3306",
|
||||
},
|
||||
{
|
||||
"localhost",
|
||||
"127.0.0.1:3306",
|
||||
},
|
||||
{
|
||||
"127.0.0.1",
|
||||
"127.0.0.1:3306",
|
||||
},
|
||||
{
|
||||
"tcp(192.168.1.1:3306)/",
|
||||
"192.168.1.1:3306",
|
||||
},
|
||||
{
|
||||
"tcp(localhost)/",
|
||||
"localhost",
|
||||
},
|
||||
{
|
||||
"root:passwd@tcp(192.168.1.1:3306)/?tls=false",
|
||||
"192.168.1.1:3306",
|
||||
},
|
||||
{
|
||||
"root@tcp(127.0.0.1:3306)/?tls=false",
|
||||
"127.0.0.1:3306",
|
||||
},
|
||||
{
|
||||
"root:passwd@tcp(localhost:3036)/dbname?allowOldPasswords=1",
|
||||
"localhost:3036",
|
||||
},
|
||||
{
|
||||
"root:foo@bar@tcp(192.1.1.1:3306)/?tls=false",
|
||||
"192.1.1.1:3306",
|
||||
},
|
||||
{
|
||||
"root:f00@b4r@tcp(192.1.1.1:3306)/?tls=false",
|
||||
"192.1.1.1:3306",
|
||||
},
|
||||
{
|
||||
"root:fl!p11@tcp(192.1.1.1:3306)/?tls=false",
|
||||
"192.1.1.1:3306",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
output, _ := parseDSN(test.input)
|
||||
if output != test.output {
|
||||
t.Errorf("Expected %s, got %s\n", test.output, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
85
plugins/inputs/mysql/parse_dsn.go
Normal file
85
plugins/inputs/mysql/parse_dsn.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseDSN parses the DSN string to a config
|
||||
func parseDSN(dsn string) (string, error) {
|
||||
//var user, passwd string
|
||||
var addr, net string
|
||||
|
||||
// [user[:password]@][net[(addr)]]/dbname[?param1=value1¶mN=valueN]
|
||||
// Find the last '/' (since the password or the net addr might contain a '/')
|
||||
for i := len(dsn) - 1; i >= 0; i-- {
|
||||
if dsn[i] == '/' {
|
||||
var j, k int
|
||||
|
||||
// left part is empty if i <= 0
|
||||
if i > 0 {
|
||||
// [username[:password]@][protocol[(address)]]
|
||||
// Find the last '@' in dsn[:i]
|
||||
for j = i; j >= 0; j-- {
|
||||
if dsn[j] == '@' {
|
||||
// username[:password]
|
||||
// Find the first ':' in dsn[:j]
|
||||
for k = 0; k < j; k++ {
|
||||
if dsn[k] == ':' {
|
||||
//passwd = dsn[k+1 : j]
|
||||
break
|
||||
}
|
||||
}
|
||||
//user = dsn[:k]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// [protocol[(address)]]
|
||||
// Find the first '(' in dsn[j+1:i]
|
||||
for k = j + 1; k < i; k++ {
|
||||
if dsn[k] == '(' {
|
||||
// dsn[i-1] must be == ')' if an address is specified
|
||||
if dsn[i-1] != ')' {
|
||||
if strings.ContainsRune(dsn[k+1:i], ')') {
|
||||
return "", errors.New("Invalid DSN unescaped")
|
||||
}
|
||||
return "", errors.New("Invalid DSN Addr")
|
||||
}
|
||||
addr = dsn[k+1 : i-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
net = dsn[j+1 : k]
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Set default network if empty
|
||||
if net == "" {
|
||||
net = "tcp"
|
||||
}
|
||||
|
||||
// Set default address if empty
|
||||
if addr == "" {
|
||||
switch net {
|
||||
case "tcp":
|
||||
addr = "127.0.0.1:3306"
|
||||
case "unix":
|
||||
addr = "/tmp/mysql.sock"
|
||||
default:
|
||||
return "", errors.New("Default addr for network '" + net + "' unknown")
|
||||
}
|
||||
}
|
||||
|
||||
return addr, nil
|
||||
}
|
||||
165
plugins/inputs/nginx/nginx.go
Normal file
165
plugins/inputs/nginx/nginx.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type Nginx struct {
|
||||
Urls []string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of Nginx stub_status URI to gather stats.
|
||||
urls = ["http://localhost/status"]
|
||||
`
|
||||
|
||||
func (n *Nginx) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (n *Nginx) Description() string {
|
||||
return "Read Nginx's basic status information (ngx_http_stub_status_module)"
|
||||
}
|
||||
|
||||
func (n *Nginx) Gather(acc inputs.Accumulator) error {
|
||||
var wg sync.WaitGroup
|
||||
var outerr error
|
||||
|
||||
for _, u := range n.Urls {
|
||||
addr, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse address '%s': %s", u, err)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(addr *url.URL) {
|
||||
defer wg.Done()
|
||||
outerr = n.gatherUrl(addr, acc)
|
||||
}(addr)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
var tr = &http.Transport{
|
||||
ResponseHeaderTimeout: time.Duration(3 * time.Second),
|
||||
}
|
||||
|
||||
var client = &http.Client{Transport: tr}
|
||||
|
||||
func (n *Nginx) gatherUrl(addr *url.URL, acc inputs.Accumulator) error {
|
||||
resp, err := client.Get(addr.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error making HTTP request to %s: %s", addr.String(), err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%s returned HTTP status %s", addr.String(), resp.Status)
|
||||
}
|
||||
r := bufio.NewReader(resp.Body)
|
||||
|
||||
// Active connections
|
||||
_, err = r.ReadString(':')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
active, err := strconv.ParseUint(strings.TrimSpace(line), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Server accepts handled requests
|
||||
_, err = r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
line, err = r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := strings.SplitN(strings.TrimSpace(line), " ", 3)
|
||||
accepts, err := strconv.ParseUint(data[0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handled, err := strconv.ParseUint(data[1], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requests, err := strconv.ParseUint(data[2], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reading/Writing/Waiting
|
||||
line, err = r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = strings.SplitN(strings.TrimSpace(line), " ", 6)
|
||||
reading, err := strconv.ParseUint(data[1], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writing, err := strconv.ParseUint(data[3], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
waiting, err := strconv.ParseUint(data[5], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags := getTags(addr)
|
||||
fields := map[string]interface{}{
|
||||
"active": active,
|
||||
"accepts": accepts,
|
||||
"handled": handled,
|
||||
"requests": requests,
|
||||
"reading": reading,
|
||||
"writing": writing,
|
||||
"waiting": waiting,
|
||||
}
|
||||
acc.AddFields("nginx", fields, tags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get tag(s) for the nginx plugin
|
||||
func getTags(addr *url.URL) map[string]string {
|
||||
h := addr.Host
|
||||
host, port, err := net.SplitHostPort(h)
|
||||
if err != nil {
|
||||
host = addr.Host
|
||||
if addr.Scheme == "http" {
|
||||
port = "80"
|
||||
} else if addr.Scheme == "https" {
|
||||
port = "443"
|
||||
} else {
|
||||
port = ""
|
||||
}
|
||||
}
|
||||
return map[string]string{"server": host, "port": port}
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("nginx", func() inputs.Input {
|
||||
return &Nginx{}
|
||||
})
|
||||
}
|
||||
85
plugins/inputs/nginx/nginx_test.go
Normal file
85
plugins/inputs/nginx/nginx_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const sampleResponse = `
|
||||
Active connections: 585
|
||||
server accepts handled requests
|
||||
85340 85340 35085
|
||||
Reading: 4 Writing: 135 Waiting: 446
|
||||
`
|
||||
|
||||
// Verify that nginx tags are properly parsed based on the server
|
||||
func TestNginxTags(t *testing.T) {
|
||||
urls := []string{"http://localhost/endpoint", "http://localhost:80/endpoint"}
|
||||
var addr *url.URL
|
||||
for _, url1 := range urls {
|
||||
addr, _ = url.Parse(url1)
|
||||
tagMap := getTags(addr)
|
||||
assert.Contains(t, tagMap["server"], "localhost")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxGeneratesMetrics(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var rsp string
|
||||
|
||||
if r.URL.Path == "/stub_status" {
|
||||
rsp = sampleResponse
|
||||
} else {
|
||||
panic("Cannot handle request")
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, rsp)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
n := &Nginx{
|
||||
Urls: []string{fmt.Sprintf("%s/stub_status", ts.URL)},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := n.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"active": uint64(585),
|
||||
"accepts": uint64(85340),
|
||||
"handled": uint64(85340),
|
||||
"requests": uint64(35085),
|
||||
"reading": uint64(4),
|
||||
"writing": uint64(135),
|
||||
"waiting": uint64(446),
|
||||
}
|
||||
addr, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(addr.Host)
|
||||
if err != nil {
|
||||
host = addr.Host
|
||||
if addr.Scheme == "http" {
|
||||
port = "80"
|
||||
} else if addr.Scheme == "https" {
|
||||
port = "443"
|
||||
} else {
|
||||
port = ""
|
||||
}
|
||||
}
|
||||
|
||||
tags := map[string]string{"server": host, "port": port}
|
||||
acc.AssertContainsTaggedFields(t, "nginx", fields, tags)
|
||||
}
|
||||
85
plugins/inputs/phpfpm/README.md
Normal file
85
plugins/inputs/phpfpm/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Telegraf plugin: phpfpm
|
||||
|
||||
Get phpfpm stat using either HTTP status page or fpm socket.
|
||||
|
||||
# Measurements
|
||||
|
||||
Meta:
|
||||
|
||||
- tags: `url=<ip> pool=poolname`
|
||||
|
||||
Measurement names:
|
||||
|
||||
- accepted_conn
|
||||
- listen_queue
|
||||
- max_listen_queue
|
||||
- listen_queue_len
|
||||
- idle_processes
|
||||
- active_processes
|
||||
- total_processes
|
||||
- max_active_processes
|
||||
- max_children_reached
|
||||
- slow_requests
|
||||
|
||||
# Example output
|
||||
|
||||
Using this configuration:
|
||||
|
||||
```
|
||||
[phpfpm]
|
||||
# An array of address to gather stats about. Specify an ip on hostname
|
||||
# with optional port and path. ie localhost, 10.10.3.33/server-status, etc.
|
||||
#
|
||||
# We can configure in three modes:
|
||||
# - unixsocket: the string is the path to fpm socket like
|
||||
# /var/run/php5-fpm.sock
|
||||
# - http: the URL has to start with http:// or https://
|
||||
# - fcgi: the URL has to start with fcgi:// or cgi://, and socket port must present
|
||||
#
|
||||
# If no servers are specified, then default to 127.0.0.1/server-status
|
||||
urls = ["http://localhost/status", "10.0.0.12:/var/run/php5-fpm-www2.sock", "fcgi://10.0.0.12:9000/status"]
|
||||
```
|
||||
|
||||
When run with:
|
||||
|
||||
```
|
||||
./telegraf -config telegraf.conf -input-filter phpfpm -test
|
||||
```
|
||||
|
||||
It produces:
|
||||
|
||||
```
|
||||
* Plugin: phpfpm, Collection 1
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_idle_processes value=1
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_total_processes value=2
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_max_children_reached value=0
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_max_listen_queue value=0
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_listen_queue value=0
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_listen_queue_len value=0
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_active_processes value=1
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_max_active_processes value=2
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_slow_requests value=0
|
||||
> [url="10.0.0.12" pool="www"] phpfpm_accepted_conn value=305
|
||||
|
||||
> [url="localhost" pool="www2"] phpfpm_max_children_reached value=0
|
||||
> [url="localhost" pool="www2"] phpfpm_slow_requests value=0
|
||||
> [url="localhost" pool="www2"] phpfpm_max_listen_queue value=0
|
||||
> [url="localhost" pool="www2"] phpfpm_active_processes value=1
|
||||
> [url="localhost" pool="www2"] phpfpm_listen_queue_len value=0
|
||||
> [url="localhost" pool="www2"] phpfpm_idle_processes value=1
|
||||
> [url="localhost" pool="www2"] phpfpm_total_processes value=2
|
||||
> [url="localhost" pool="www2"] phpfpm_max_active_processes value=2
|
||||
> [url="localhost" pool="www2"] phpfpm_accepted_conn value=306
|
||||
> [url="localhost" pool="www2"] phpfpm_listen_queue value=0
|
||||
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_max_children_reached value=0
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_slow_requests value=1
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_max_listen_queue value=0
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_active_processes value=1
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_listen_queue_len value=0
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_idle_processes value=2
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_total_processes value=2
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_max_active_processes value=2
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_accepted_conn value=307
|
||||
> [url="10.0.0.12:9000" pool="www3"] phpfpm_listen_queue value=0
|
||||
```
|
||||
215
plugins/inputs/phpfpm/phpfpm.go
Normal file
215
plugins/inputs/phpfpm/phpfpm.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package phpfpm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
const (
|
||||
PF_POOL = "pool"
|
||||
PF_PROCESS_MANAGER = "process manager"
|
||||
PF_ACCEPTED_CONN = "accepted conn"
|
||||
PF_LISTEN_QUEUE = "listen queue"
|
||||
PF_MAX_LISTEN_QUEUE = "max listen queue"
|
||||
PF_LISTEN_QUEUE_LEN = "listen queue len"
|
||||
PF_IDLE_PROCESSES = "idle processes"
|
||||
PF_ACTIVE_PROCESSES = "active processes"
|
||||
PF_TOTAL_PROCESSES = "total processes"
|
||||
PF_MAX_ACTIVE_PROCESSES = "max active processes"
|
||||
PF_MAX_CHILDREN_REACHED = "max children reached"
|
||||
PF_SLOW_REQUESTS = "slow requests"
|
||||
)
|
||||
|
||||
type metric map[string]int64
|
||||
type poolStat map[string]metric
|
||||
|
||||
type phpfpm struct {
|
||||
Urls []string
|
||||
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of addresses to gather stats about. Specify an ip or hostname
|
||||
# with optional port and path.
|
||||
#
|
||||
# Plugin can be configured in three modes (both can be used):
|
||||
# - http: the URL must start with http:// or https://, ex:
|
||||
# "http://localhost/status"
|
||||
# "http://192.168.130.1/status?full"
|
||||
# - unixsocket: path to fpm socket, ex:
|
||||
# "/var/run/php5-fpm.sock"
|
||||
# "192.168.10.10:/var/run/php5-fpm-www2.sock"
|
||||
# - fcgi: the URL mush start with fcgi:// or cgi://, and port must present, ex:
|
||||
# "fcgi://10.0.0.12:9000/status"
|
||||
# "cgi://10.0.10.12:9001/status"
|
||||
#
|
||||
# If no servers are specified, then default to 127.0.0.1/server-status
|
||||
urls = ["http://localhost/status"]
|
||||
`
|
||||
|
||||
func (r *phpfpm) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (r *phpfpm) Description() string {
|
||||
return "Read metrics of phpfpm, via HTTP status page or socket(pending)"
|
||||
}
|
||||
|
||||
// Reads stats from all configured servers accumulates stats.
|
||||
// Returns one of the errors encountered while gather stats (if any).
|
||||
func (g *phpfpm) Gather(acc inputs.Accumulator) error {
|
||||
if len(g.Urls) == 0 {
|
||||
return g.gatherServer("http://127.0.0.1/status", acc)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var outerr error
|
||||
|
||||
for _, serv := range g.Urls {
|
||||
wg.Add(1)
|
||||
go func(serv string) {
|
||||
defer wg.Done()
|
||||
outerr = g.gatherServer(serv, acc)
|
||||
}(serv)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
// Request status page to get stat raw data
|
||||
func (g *phpfpm) gatherServer(addr string, acc inputs.Accumulator) error {
|
||||
if g.client == nil {
|
||||
|
||||
client := &http.Client{}
|
||||
g.client = client
|
||||
}
|
||||
|
||||
if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") {
|
||||
u, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable parse server address '%s': %s", addr, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s://%s%s", u.Scheme,
|
||||
u.Host, u.Path), nil)
|
||||
res, err := g.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to connect to phpfpm status page '%s': %v",
|
||||
addr, err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("Unable to get valid stat result from '%s': %v",
|
||||
addr, err)
|
||||
}
|
||||
|
||||
importMetric(res.Body, acc, u.Host)
|
||||
} else {
|
||||
var (
|
||||
fcgi *FCGIClient
|
||||
fcgiAddr string
|
||||
)
|
||||
if strings.HasPrefix(addr, "fcgi://") || strings.HasPrefix(addr, "cgi://") {
|
||||
u, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable parse server address '%s': %s", addr, err)
|
||||
}
|
||||
socketAddr := strings.Split(u.Host, ":")
|
||||
fcgiIp := socketAddr[0]
|
||||
fcgiPort, _ := strconv.Atoi(socketAddr[1])
|
||||
fcgiAddr = u.Host
|
||||
fcgi, _ = NewClient(fcgiIp, fcgiPort)
|
||||
} else {
|
||||
socketAddr := strings.Split(addr, ":")
|
||||
fcgiAddr = socketAddr[0]
|
||||
fcgi, _ = NewClient("unix", socketAddr[1])
|
||||
}
|
||||
resOut, resErr, err := fcgi.Request(map[string]string{
|
||||
"SCRIPT_NAME": "/status",
|
||||
"SCRIPT_FILENAME": "status",
|
||||
"REQUEST_METHOD": "GET",
|
||||
}, "")
|
||||
|
||||
if len(resErr) == 0 && err == nil {
|
||||
importMetric(bytes.NewReader(resOut), acc, fcgiAddr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Import HTTP stat data into Telegraf system
|
||||
func importMetric(r io.Reader, acc inputs.Accumulator, host string) (poolStat, error) {
|
||||
stats := make(poolStat)
|
||||
var currentPool string
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
statLine := scanner.Text()
|
||||
keyvalue := strings.Split(statLine, ":")
|
||||
|
||||
if len(keyvalue) < 2 {
|
||||
continue
|
||||
}
|
||||
fieldName := strings.Trim(keyvalue[0], " ")
|
||||
// We start to gather data for a new pool here
|
||||
if fieldName == PF_POOL {
|
||||
currentPool = strings.Trim(keyvalue[1], " ")
|
||||
stats[currentPool] = make(metric)
|
||||
continue
|
||||
}
|
||||
|
||||
// Start to parse metric for current pool
|
||||
switch fieldName {
|
||||
case PF_ACCEPTED_CONN,
|
||||
PF_LISTEN_QUEUE,
|
||||
PF_MAX_LISTEN_QUEUE,
|
||||
PF_LISTEN_QUEUE_LEN,
|
||||
PF_IDLE_PROCESSES,
|
||||
PF_ACTIVE_PROCESSES,
|
||||
PF_TOTAL_PROCESSES,
|
||||
PF_MAX_ACTIVE_PROCESSES,
|
||||
PF_MAX_CHILDREN_REACHED,
|
||||
PF_SLOW_REQUESTS:
|
||||
fieldValue, err := strconv.ParseInt(strings.Trim(keyvalue[1], " "), 10, 64)
|
||||
if err == nil {
|
||||
stats[currentPool][fieldName] = fieldValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, we push the pool metric
|
||||
for pool := range stats {
|
||||
tags := map[string]string{
|
||||
"url": host,
|
||||
"pool": pool,
|
||||
}
|
||||
fields := make(map[string]interface{})
|
||||
for k, v := range stats[pool] {
|
||||
fields[strings.Replace(k, " ", "_", -1)] = v
|
||||
}
|
||||
acc.AddFields("phpfpm", fields, tags)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("phpfpm", func() inputs.Input {
|
||||
return &phpfpm{}
|
||||
})
|
||||
}
|
||||
321
plugins/inputs/phpfpm/phpfpm_fcgi.go
Normal file
321
plugins/inputs/phpfpm/phpfpm_fcgi.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package phpfpm
|
||||
|
||||
// FastCGI client to request via socket
|
||||
|
||||
// Copyright 2012 Junqing Tan <ivan@mysqlab.net> and The Go Authors
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// Part of source code is from Go fcgi package
|
||||
|
||||
// Fix bug: Can't recive more than 1 record untill FCGI_END_REQUEST 2012-09-15
|
||||
// By: wofeiwo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const FCGI_LISTENSOCK_FILENO uint8 = 0
|
||||
const FCGI_HEADER_LEN uint8 = 8
|
||||
const VERSION_1 uint8 = 1
|
||||
const FCGI_NULL_REQUEST_ID uint8 = 0
|
||||
const FCGI_KEEP_CONN uint8 = 1
|
||||
|
||||
const (
|
||||
FCGI_BEGIN_REQUEST uint8 = iota + 1
|
||||
FCGI_ABORT_REQUEST
|
||||
FCGI_END_REQUEST
|
||||
FCGI_PARAMS
|
||||
FCGI_STDIN
|
||||
FCGI_STDOUT
|
||||
FCGI_STDERR
|
||||
FCGI_DATA
|
||||
FCGI_GET_VALUES
|
||||
FCGI_GET_VALUES_RESULT
|
||||
FCGI_UNKNOWN_TYPE
|
||||
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
|
||||
)
|
||||
|
||||
const (
|
||||
FCGI_RESPONDER uint8 = iota + 1
|
||||
FCGI_AUTHORIZER
|
||||
FCGI_FILTER
|
||||
)
|
||||
|
||||
const (
|
||||
FCGI_REQUEST_COMPLETE uint8 = iota
|
||||
FCGI_CANT_MPX_CONN
|
||||
FCGI_OVERLOADED
|
||||
FCGI_UNKNOWN_ROLE
|
||||
)
|
||||
|
||||
const (
|
||||
FCGI_MAX_CONNS string = "MAX_CONNS"
|
||||
FCGI_MAX_REQS string = "MAX_REQS"
|
||||
FCGI_MPXS_CONNS string = "MPXS_CONNS"
|
||||
)
|
||||
|
||||
const (
|
||||
maxWrite = 6553500 // maximum record body
|
||||
maxPad = 255
|
||||
)
|
||||
|
||||
type header struct {
|
||||
Version uint8
|
||||
Type uint8
|
||||
Id uint16
|
||||
ContentLength uint16
|
||||
PaddingLength uint8
|
||||
Reserved uint8
|
||||
}
|
||||
|
||||
// for padding so we don't have to allocate all the time
|
||||
// not synchronized because we don't care what the contents are
|
||||
var pad [maxPad]byte
|
||||
|
||||
func (h *header) init(recType uint8, reqId uint16, contentLength int) {
|
||||
h.Version = 1
|
||||
h.Type = recType
|
||||
h.Id = reqId
|
||||
h.ContentLength = uint16(contentLength)
|
||||
h.PaddingLength = uint8(-contentLength & 7)
|
||||
}
|
||||
|
||||
type record struct {
|
||||
h header
|
||||
buf [maxWrite + maxPad]byte
|
||||
}
|
||||
|
||||
func (rec *record) read(r io.Reader) (err error) {
|
||||
if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil {
|
||||
return err
|
||||
}
|
||||
if rec.h.Version != 1 {
|
||||
return errors.New("fcgi: invalid header version")
|
||||
}
|
||||
n := int(rec.h.ContentLength) + int(rec.h.PaddingLength)
|
||||
if _, err = io.ReadFull(r, rec.buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *record) content() []byte {
|
||||
return r.buf[:r.h.ContentLength]
|
||||
}
|
||||
|
||||
type FCGIClient struct {
|
||||
mutex sync.Mutex
|
||||
rwc io.ReadWriteCloser
|
||||
h header
|
||||
buf bytes.Buffer
|
||||
keepAlive bool
|
||||
}
|
||||
|
||||
func NewClient(h string, args ...interface{}) (fcgi *FCGIClient, err error) {
|
||||
var conn net.Conn
|
||||
if len(args) != 1 {
|
||||
err = errors.New("fcgi: not enough params")
|
||||
return
|
||||
}
|
||||
switch args[0].(type) {
|
||||
case int:
|
||||
addr := h + ":" + strconv.FormatInt(int64(args[0].(int)), 10)
|
||||
conn, err = net.Dial("tcp", addr)
|
||||
case string:
|
||||
laddr := net.UnixAddr{Name: args[0].(string), Net: h}
|
||||
conn, err = net.DialUnix(h, nil, &laddr)
|
||||
default:
|
||||
err = errors.New("fcgi: we only accept int (port) or string (socket) params.")
|
||||
}
|
||||
fcgi = &FCGIClient{
|
||||
rwc: conn,
|
||||
keepAlive: false,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (client *FCGIClient) writeRecord(recType uint8, reqId uint16, content []byte) (err error) {
|
||||
client.mutex.Lock()
|
||||
defer client.mutex.Unlock()
|
||||
client.buf.Reset()
|
||||
client.h.init(recType, reqId, len(content))
|
||||
if err := binary.Write(&client.buf, binary.BigEndian, client.h); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := client.buf.Write(content); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := client.buf.Write(pad[:client.h.PaddingLength]); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.rwc.Write(client.buf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func (client *FCGIClient) writeBeginRequest(reqId uint16, role uint16, flags uint8) error {
|
||||
b := [8]byte{byte(role >> 8), byte(role), flags}
|
||||
return client.writeRecord(FCGI_BEGIN_REQUEST, reqId, b[:])
|
||||
}
|
||||
|
||||
func (client *FCGIClient) writeEndRequest(reqId uint16, appStatus int, protocolStatus uint8) error {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(b, uint32(appStatus))
|
||||
b[4] = protocolStatus
|
||||
return client.writeRecord(FCGI_END_REQUEST, reqId, b)
|
||||
}
|
||||
|
||||
func (client *FCGIClient) writePairs(recType uint8, reqId uint16, pairs map[string]string) error {
|
||||
w := newWriter(client, recType, reqId)
|
||||
b := make([]byte, 8)
|
||||
for k, v := range pairs {
|
||||
n := encodeSize(b, uint32(len(k)))
|
||||
n += encodeSize(b[n:], uint32(len(v)))
|
||||
if _, err := w.Write(b[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(k); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSize(s []byte) (uint32, int) {
|
||||
if len(s) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
size, n := uint32(s[0]), 1
|
||||
if size&(1<<7) != 0 {
|
||||
if len(s) < 4 {
|
||||
return 0, 0
|
||||
}
|
||||
n = 4
|
||||
size = binary.BigEndian.Uint32(s)
|
||||
size &^= 1 << 31
|
||||
}
|
||||
return size, n
|
||||
}
|
||||
|
||||
func readString(s []byte, size uint32) string {
|
||||
if size > uint32(len(s)) {
|
||||
return ""
|
||||
}
|
||||
return string(s[:size])
|
||||
}
|
||||
|
||||
func encodeSize(b []byte, size uint32) int {
|
||||
if size > 127 {
|
||||
size |= 1 << 31
|
||||
binary.BigEndian.PutUint32(b, size)
|
||||
return 4
|
||||
}
|
||||
b[0] = byte(size)
|
||||
return 1
|
||||
}
|
||||
|
||||
// bufWriter encapsulates bufio.Writer but also closes the underlying stream when
|
||||
// Closed.
|
||||
type bufWriter struct {
|
||||
closer io.Closer
|
||||
*bufio.Writer
|
||||
}
|
||||
|
||||
func (w *bufWriter) Close() error {
|
||||
if err := w.Writer.Flush(); err != nil {
|
||||
w.closer.Close()
|
||||
return err
|
||||
}
|
||||
return w.closer.Close()
|
||||
}
|
||||
|
||||
func newWriter(c *FCGIClient, recType uint8, reqId uint16) *bufWriter {
|
||||
s := &streamWriter{c: c, recType: recType, reqId: reqId}
|
||||
w := bufio.NewWriterSize(s, maxWrite)
|
||||
return &bufWriter{s, w}
|
||||
}
|
||||
|
||||
// streamWriter abstracts out the separation of a stream into discrete records.
|
||||
// It only writes maxWrite bytes at a time.
|
||||
type streamWriter struct {
|
||||
c *FCGIClient
|
||||
recType uint8
|
||||
reqId uint16
|
||||
}
|
||||
|
||||
func (w *streamWriter) Write(p []byte) (int, error) {
|
||||
nn := 0
|
||||
for len(p) > 0 {
|
||||
n := len(p)
|
||||
if n > maxWrite {
|
||||
n = maxWrite
|
||||
}
|
||||
if err := w.c.writeRecord(w.recType, w.reqId, p[:n]); err != nil {
|
||||
return nn, err
|
||||
}
|
||||
nn += n
|
||||
p = p[n:]
|
||||
}
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
func (w *streamWriter) Close() error {
|
||||
// send empty record to close the stream
|
||||
return w.c.writeRecord(w.recType, w.reqId, nil)
|
||||
}
|
||||
|
||||
func (client *FCGIClient) Request(env map[string]string, reqStr string) (retout []byte, reterr []byte, err error) {
|
||||
|
||||
var reqId uint16 = 1
|
||||
defer client.rwc.Close()
|
||||
|
||||
err = client.writeBeginRequest(reqId, uint16(FCGI_RESPONDER), 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = client.writePairs(FCGI_PARAMS, reqId, env)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(reqStr) > 0 {
|
||||
err = client.writeRecord(FCGI_STDIN, reqId, []byte(reqStr))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rec := &record{}
|
||||
var err1 error
|
||||
|
||||
// recive untill EOF or FCGI_END_REQUEST
|
||||
for {
|
||||
err1 = rec.read(client.rwc)
|
||||
if err1 != nil {
|
||||
if err1 != io.EOF {
|
||||
err = err1
|
||||
}
|
||||
break
|
||||
}
|
||||
switch {
|
||||
case rec.h.Type == FCGI_STDOUT:
|
||||
retout = append(retout, rec.content()...)
|
||||
case rec.h.Type == FCGI_STDERR:
|
||||
reterr = append(reterr, rec.content()...)
|
||||
case rec.h.Type == FCGI_END_REQUEST:
|
||||
fallthrough
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
79
plugins/inputs/phpfpm/phpfpm_test.go
Normal file
79
plugins/inputs/phpfpm/phpfpm_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package phpfpm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
func TestPhpFpmGeneratesMetrics(t *testing.T) {
|
||||
//We create a fake server to return test data
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, outputSample)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
//Now we tested again above server, with our authentication data
|
||||
r := &phpfpm{
|
||||
Urls: []string{ts.URL},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := r.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags := map[string]string{
|
||||
"url": ts.Listener.Addr().String(),
|
||||
"pool": "www",
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"accepted_conn": int64(3),
|
||||
"listen_queue": int64(1),
|
||||
"max_listen_queue": int64(0),
|
||||
"listen_queue_len": int64(0),
|
||||
"idle_processes": int64(1),
|
||||
"active_processes": int64(1),
|
||||
"total_processes": int64(2),
|
||||
"max_active_processes": int64(1),
|
||||
"max_children_reached": int64(2),
|
||||
"slow_requests": int64(1),
|
||||
}
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "phpfpm", fields, tags)
|
||||
}
|
||||
|
||||
//When not passing server config, we default to localhost
|
||||
//We just want to make sure we did request stat from localhost
|
||||
func TestHaproxyDefaultGetFromLocalhost(t *testing.T) {
|
||||
r := &phpfpm{}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := r.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "127.0.0.1/status")
|
||||
}
|
||||
|
||||
const outputSample = `
|
||||
pool: www
|
||||
process manager: dynamic
|
||||
start time: 11/Oct/2015:23:38:51 +0000
|
||||
start since: 1991
|
||||
accepted conn: 3
|
||||
listen queue: 1
|
||||
max listen queue: 0
|
||||
listen queue len: 0
|
||||
idle processes: 1
|
||||
active processes: 1
|
||||
total processes: 2
|
||||
max active processes: 1
|
||||
max children reached: 2
|
||||
slow requests: 1
|
||||
`
|
||||
180
plugins/inputs/ping/ping.go
Normal file
180
plugins/inputs/ping/ping.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package ping
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
// HostPinger is a function that runs the "ping" function using a list of
|
||||
// passed arguments. This can be easily switched with a mocked ping function
|
||||
// for unit test purposes (see ping_test.go)
|
||||
type HostPinger func(args ...string) (string, error)
|
||||
|
||||
type Ping struct {
|
||||
// Interval at which to ping (ping -i <INTERVAL>)
|
||||
PingInterval float64 `toml:"ping_interval"`
|
||||
|
||||
// Number of pings to send (ping -c <COUNT>)
|
||||
Count int
|
||||
|
||||
// Ping timeout, in seconds. 0 means no timeout (ping -t <TIMEOUT>)
|
||||
Timeout float64
|
||||
|
||||
// Interface to send ping from (ping -I <INTERFACE>)
|
||||
Interface string
|
||||
|
||||
// URLs to ping
|
||||
Urls []string
|
||||
|
||||
// host ping function
|
||||
pingHost HostPinger
|
||||
}
|
||||
|
||||
func (_ *Ping) Description() string {
|
||||
return "Ping given url(s) and return statistics"
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# urls to ping
|
||||
urls = ["www.google.com"] # required
|
||||
# number of pings to send (ping -c <COUNT>)
|
||||
count = 1 # required
|
||||
# interval, in s, at which to ping. 0 == default (ping -i <PING_INTERVAL>)
|
||||
ping_interval = 0.0
|
||||
# ping timeout, in s. 0 == no timeout (ping -t <TIMEOUT>)
|
||||
timeout = 0.0
|
||||
# interface to send ping from (ping -I <INTERFACE>)
|
||||
interface = ""
|
||||
`
|
||||
|
||||
func (_ *Ping) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (p *Ping) Gather(acc inputs.Accumulator) error {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errorChannel := make(chan error, len(p.Urls)*2)
|
||||
|
||||
// Spin off a go routine for each url to ping
|
||||
for _, url := range p.Urls {
|
||||
wg.Add(1)
|
||||
go func(url string, acc inputs.Accumulator) {
|
||||
defer wg.Done()
|
||||
args := p.args(url)
|
||||
out, err := p.pingHost(args...)
|
||||
if err != nil {
|
||||
// Combine go err + stderr output
|
||||
errorChannel <- errors.New(
|
||||
strings.TrimSpace(out) + ", " + err.Error())
|
||||
}
|
||||
tags := map[string]string{"url": url}
|
||||
trans, rec, avg, err := processPingOutput(out)
|
||||
if err != nil {
|
||||
// fatal error
|
||||
errorChannel <- err
|
||||
return
|
||||
}
|
||||
// Calculate packet loss percentage
|
||||
loss := float64(trans-rec) / float64(trans) * 100.0
|
||||
fields := map[string]interface{}{
|
||||
"packets_transmitted": trans,
|
||||
"packets_received": rec,
|
||||
"percent_packet_loss": loss,
|
||||
"average_response_ms": avg,
|
||||
}
|
||||
acc.AddFields("ping", fields, tags)
|
||||
}(url, acc)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errorChannel)
|
||||
|
||||
// Get all errors and return them as one giant error
|
||||
errorStrings := []string{}
|
||||
for err := range errorChannel {
|
||||
errorStrings = append(errorStrings, err.Error())
|
||||
}
|
||||
|
||||
if len(errorStrings) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(strings.Join(errorStrings, "\n"))
|
||||
}
|
||||
|
||||
func hostPinger(args ...string) (string, error) {
|
||||
c := exec.Command("ping", args...)
|
||||
out, err := c.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
// args returns the arguments for the 'ping' executable
|
||||
func (p *Ping) args(url string) []string {
|
||||
// Build the ping command args based on toml config
|
||||
args := []string{"-c", strconv.Itoa(p.Count)}
|
||||
if p.PingInterval > 0 {
|
||||
args = append(args, "-i", strconv.FormatFloat(p.PingInterval, 'f', 1, 64))
|
||||
}
|
||||
if p.Timeout > 0 {
|
||||
args = append(args, "-t", strconv.FormatFloat(p.Timeout, 'f', 1, 64))
|
||||
}
|
||||
if p.Interface != "" {
|
||||
args = append(args, "-I", p.Interface)
|
||||
}
|
||||
args = append(args, url)
|
||||
return args
|
||||
}
|
||||
|
||||
// processPingOutput takes in a string output from the ping command, like:
|
||||
//
|
||||
// PING www.google.com (173.194.115.84): 56 data bytes
|
||||
// 64 bytes from 173.194.115.84: icmp_seq=0 ttl=54 time=52.172 ms
|
||||
// 64 bytes from 173.194.115.84: icmp_seq=1 ttl=54 time=34.843 ms
|
||||
//
|
||||
// --- www.google.com ping statistics ---
|
||||
// 2 packets transmitted, 2 packets received, 0.0% packet loss
|
||||
// round-trip min/avg/max/stddev = 34.843/43.508/52.172/8.664 ms
|
||||
//
|
||||
// It returns (<transmitted packets>, <received packets>, <average response>)
|
||||
func processPingOutput(out string) (int, int, float64, error) {
|
||||
var trans, recv int
|
||||
var avg float64
|
||||
// Set this error to nil if we find a 'transmitted' line
|
||||
err := errors.New("Fatal error processing ping output")
|
||||
lines := strings.Split(out, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "transmitted") &&
|
||||
strings.Contains(line, "received") {
|
||||
err = nil
|
||||
stats := strings.Split(line, ", ")
|
||||
// Transmitted packets
|
||||
trans, err = strconv.Atoi(strings.Split(stats[0], " ")[0])
|
||||
if err != nil {
|
||||
return trans, recv, avg, err
|
||||
}
|
||||
// Received packets
|
||||
recv, err = strconv.Atoi(strings.Split(stats[1], " ")[0])
|
||||
if err != nil {
|
||||
return trans, recv, avg, err
|
||||
}
|
||||
} else if strings.Contains(line, "min/avg/max") {
|
||||
stats := strings.Split(line, " = ")[1]
|
||||
avg, err = strconv.ParseFloat(strings.Split(stats, "/")[1], 64)
|
||||
if err != nil {
|
||||
return trans, recv, avg, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return trans, recv, avg, err
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("ping", func() inputs.Input {
|
||||
return &Ping{pingHost: hostPinger}
|
||||
})
|
||||
}
|
||||
222
plugins/inputs/ping/ping_test.go
Normal file
222
plugins/inputs/ping/ping_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package ping
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// BSD/Darwin ping output
|
||||
var bsdPingOutput = `
|
||||
PING www.google.com (216.58.217.36): 56 data bytes
|
||||
64 bytes from 216.58.217.36: icmp_seq=0 ttl=55 time=15.087 ms
|
||||
64 bytes from 216.58.217.36: icmp_seq=1 ttl=55 time=21.564 ms
|
||||
64 bytes from 216.58.217.36: icmp_seq=2 ttl=55 time=27.263 ms
|
||||
64 bytes from 216.58.217.36: icmp_seq=3 ttl=55 time=18.828 ms
|
||||
64 bytes from 216.58.217.36: icmp_seq=4 ttl=55 time=18.378 ms
|
||||
|
||||
--- www.google.com ping statistics ---
|
||||
5 packets transmitted, 5 packets received, 0.0% packet loss
|
||||
round-trip min/avg/max/stddev = 15.087/20.224/27.263/4.076 ms
|
||||
`
|
||||
|
||||
// Linux ping output
|
||||
var linuxPingOutput = `
|
||||
PING www.google.com (216.58.218.164) 56(84) bytes of data.
|
||||
64 bytes from host.net (216.58.218.164): icmp_seq=1 ttl=63 time=35.2 ms
|
||||
64 bytes from host.net (216.58.218.164): icmp_seq=2 ttl=63 time=42.3 ms
|
||||
64 bytes from host.net (216.58.218.164): icmp_seq=3 ttl=63 time=45.1 ms
|
||||
64 bytes from host.net (216.58.218.164): icmp_seq=4 ttl=63 time=43.5 ms
|
||||
64 bytes from host.net (216.58.218.164): icmp_seq=5 ttl=63 time=51.8 ms
|
||||
|
||||
--- www.google.com ping statistics ---
|
||||
5 packets transmitted, 5 received, 0% packet loss, time 4010ms
|
||||
rtt min/avg/max/mdev = 35.225/43.628/51.806/5.325 ms
|
||||
`
|
||||
|
||||
// Fatal ping output (invalid argument)
|
||||
var fatalPingOutput = `
|
||||
ping: -i interval too short: Operation not permitted
|
||||
`
|
||||
|
||||
// Test that ping command output is processed properly
|
||||
func TestProcessPingOutput(t *testing.T) {
|
||||
trans, rec, avg, err := processPingOutput(bsdPingOutput)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, trans, "5 packets were transmitted")
|
||||
assert.Equal(t, 5, rec, "5 packets were transmitted")
|
||||
assert.InDelta(t, 20.224, avg, 0.001)
|
||||
|
||||
trans, rec, avg, err = processPingOutput(linuxPingOutput)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, trans, "5 packets were transmitted")
|
||||
assert.Equal(t, 5, rec, "5 packets were transmitted")
|
||||
assert.InDelta(t, 43.628, avg, 0.001)
|
||||
}
|
||||
|
||||
// Test that processPingOutput returns an error when 'ping' fails to run, such
|
||||
// as when an invalid argument is provided
|
||||
func TestErrorProcessPingOutput(t *testing.T) {
|
||||
_, _, _, err := processPingOutput(fatalPingOutput)
|
||||
assert.Error(t, err, "Error was expected from processPingOutput")
|
||||
}
|
||||
|
||||
// Test that arg lists and created correctly
|
||||
func TestArgs(t *testing.T) {
|
||||
p := Ping{
|
||||
Count: 2,
|
||||
}
|
||||
|
||||
// Actual and Expected arg lists must be sorted for reflect.DeepEqual
|
||||
|
||||
actual := p.args("www.google.com")
|
||||
expected := []string{"-c", "2", "www.google.com"}
|
||||
sort.Strings(actual)
|
||||
sort.Strings(expected)
|
||||
assert.True(t, reflect.DeepEqual(expected, actual),
|
||||
"Expected: %s Actual: %s", expected, actual)
|
||||
|
||||
p.Interface = "eth0"
|
||||
actual = p.args("www.google.com")
|
||||
expected = []string{"-c", "2", "-I", "eth0", "www.google.com"}
|
||||
sort.Strings(actual)
|
||||
sort.Strings(expected)
|
||||
assert.True(t, reflect.DeepEqual(expected, actual),
|
||||
"Expected: %s Actual: %s", expected, actual)
|
||||
|
||||
p.Timeout = 12.0
|
||||
actual = p.args("www.google.com")
|
||||
expected = []string{"-c", "2", "-I", "eth0", "-t", "12.0", "www.google.com"}
|
||||
sort.Strings(actual)
|
||||
sort.Strings(expected)
|
||||
assert.True(t, reflect.DeepEqual(expected, actual),
|
||||
"Expected: %s Actual: %s", expected, actual)
|
||||
|
||||
p.PingInterval = 1.2
|
||||
actual = p.args("www.google.com")
|
||||
expected = []string{"-c", "2", "-I", "eth0", "-t", "12.0", "-i", "1.2",
|
||||
"www.google.com"}
|
||||
sort.Strings(actual)
|
||||
sort.Strings(expected)
|
||||
assert.True(t, reflect.DeepEqual(expected, actual),
|
||||
"Expected: %s Actual: %s", expected, actual)
|
||||
}
|
||||
|
||||
func mockHostPinger(args ...string) (string, error) {
|
||||
return linuxPingOutput, nil
|
||||
}
|
||||
|
||||
// Test that Gather function works on a normal ping
|
||||
func TestPingGather(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
p := Ping{
|
||||
Urls: []string{"www.google.com", "www.reddit.com"},
|
||||
pingHost: mockHostPinger,
|
||||
}
|
||||
|
||||
p.Gather(&acc)
|
||||
tags := map[string]string{"url": "www.google.com"}
|
||||
fields := map[string]interface{}{
|
||||
"packets_transmitted": 5,
|
||||
"packets_received": 5,
|
||||
"percent_packet_loss": 0.0,
|
||||
"average_response_ms": 43.628,
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "ping", fields, tags)
|
||||
|
||||
tags = map[string]string{"url": "www.reddit.com"}
|
||||
acc.AssertContainsTaggedFields(t, "ping", fields, tags)
|
||||
}
|
||||
|
||||
var lossyPingOutput = `
|
||||
PING www.google.com (216.58.218.164) 56(84) bytes of data.
|
||||
64 bytes from host.net (216.58.218.164): icmp_seq=1 ttl=63 time=35.2 ms
|
||||
64 bytes from host.net (216.58.218.164): icmp_seq=3 ttl=63 time=45.1 ms
|
||||
64 bytes from host.net (216.58.218.164): icmp_seq=5 ttl=63 time=51.8 ms
|
||||
|
||||
--- www.google.com ping statistics ---
|
||||
5 packets transmitted, 3 received, 40% packet loss, time 4010ms
|
||||
rtt min/avg/max/mdev = 35.225/44.033/51.806/5.325 ms
|
||||
`
|
||||
|
||||
func mockLossyHostPinger(args ...string) (string, error) {
|
||||
return lossyPingOutput, nil
|
||||
}
|
||||
|
||||
// Test that Gather works on a ping with lossy packets
|
||||
func TestLossyPingGather(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
p := Ping{
|
||||
Urls: []string{"www.google.com"},
|
||||
pingHost: mockLossyHostPinger,
|
||||
}
|
||||
|
||||
p.Gather(&acc)
|
||||
tags := map[string]string{"url": "www.google.com"}
|
||||
fields := map[string]interface{}{
|
||||
"packets_transmitted": 5,
|
||||
"packets_received": 3,
|
||||
"percent_packet_loss": 40.0,
|
||||
"average_response_ms": 44.033,
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "ping", fields, tags)
|
||||
}
|
||||
|
||||
var errorPingOutput = `
|
||||
PING www.amazon.com (176.32.98.166): 56 data bytes
|
||||
Request timeout for icmp_seq 0
|
||||
|
||||
--- www.amazon.com ping statistics ---
|
||||
2 packets transmitted, 0 packets received, 100.0% packet loss
|
||||
`
|
||||
|
||||
func mockErrorHostPinger(args ...string) (string, error) {
|
||||
return errorPingOutput, errors.New("No packets received")
|
||||
}
|
||||
|
||||
// Test that Gather works on a ping with no transmitted packets, even though the
|
||||
// command returns an error
|
||||
func TestBadPingGather(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
p := Ping{
|
||||
Urls: []string{"www.amazon.com"},
|
||||
pingHost: mockErrorHostPinger,
|
||||
}
|
||||
|
||||
p.Gather(&acc)
|
||||
tags := map[string]string{"url": "www.amazon.com"}
|
||||
fields := map[string]interface{}{
|
||||
"packets_transmitted": 2,
|
||||
"packets_received": 0,
|
||||
"percent_packet_loss": 100.0,
|
||||
"average_response_ms": 0.0,
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "ping", fields, tags)
|
||||
}
|
||||
|
||||
func mockFatalHostPinger(args ...string) (string, error) {
|
||||
return fatalPingOutput, errors.New("So very bad")
|
||||
}
|
||||
|
||||
// Test that a fatal ping command does not gather any statistics.
|
||||
func TestFatalPingGather(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
p := Ping{
|
||||
Urls: []string{"www.amazon.com"},
|
||||
pingHost: mockFatalHostPinger,
|
||||
}
|
||||
|
||||
p.Gather(&acc)
|
||||
assert.False(t, acc.HasMeasurement("packets_transmitted"),
|
||||
"Fatal ping should not have packet measurements")
|
||||
assert.False(t, acc.HasMeasurement("packets_received"),
|
||||
"Fatal ping should not have packet measurements")
|
||||
assert.False(t, acc.HasMeasurement("percent_packet_loss"),
|
||||
"Fatal ping should not have packet measurements")
|
||||
assert.False(t, acc.HasMeasurement("average_response_ms"),
|
||||
"Fatal ping should not have packet measurements")
|
||||
}
|
||||
30
plugins/inputs/postgresql/README.md
Normal file
30
plugins/inputs/postgresql/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# PostgreSQL plugin
|
||||
|
||||
This postgresql plugin provides metrics for your postgres database. It currently works with postgres versions 8.1+. It uses data from the built in _pg_stat_database_ view. The metrics recorded depend on your version of postgres. See table:
|
||||
```
|
||||
pg version 9.2+ 9.1 8.3-9.0 8.1-8.2 7.4-8.0(unsupported)
|
||||
--- --- --- ------- ------- -------
|
||||
datid* x x x x
|
||||
datname* x x x x
|
||||
numbackends x x x x x
|
||||
xact_commit x x x x x
|
||||
xact_rollback x x x x x
|
||||
blks_read x x x x x
|
||||
blks_hit x x x x x
|
||||
tup_returned x x x
|
||||
tup_fetched x x x
|
||||
tup_inserted x x x
|
||||
tup_updated x x x
|
||||
tup_deleted x x x
|
||||
conflicts x x
|
||||
temp_files x
|
||||
temp_bytes x
|
||||
deadlocks x
|
||||
blk_read_time x
|
||||
blk_write_time x
|
||||
stats_reset* x x
|
||||
```
|
||||
|
||||
_* value ignored and therefore not recorded._
|
||||
|
||||
More information about the meaning of these metrics can be found in the [PostgreSQL Documentation](http://www.postgresql.org/docs/9.2/static/monitoring-stats.html#PG-STAT-DATABASE-VIEW)
|
||||
151
plugins/inputs/postgresql/postgresql.go
Normal file
151
plugins/inputs/postgresql/postgresql.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Postgresql struct {
|
||||
Address string
|
||||
Databases []string
|
||||
OrderedColumns []string
|
||||
}
|
||||
|
||||
var ignoredColumns = map[string]bool{"datid": true, "datname": true, "stats_reset": true}
|
||||
|
||||
var sampleConfig = `
|
||||
# specify address via a url matching:
|
||||
# postgres://[pqgotest[:password]]@localhost[/dbname]?sslmode=[disable|verify-ca|verify-full]
|
||||
# or a simple string:
|
||||
# host=localhost user=pqotest password=... sslmode=... dbname=app_production
|
||||
#
|
||||
# All connection parameters are optional.
|
||||
#
|
||||
# Without the dbname parameter, the driver will default to a database
|
||||
# with the same name as the user. This dbname is just for instantiating a
|
||||
# connection with the server and doesn't restrict the databases we are trying
|
||||
# to grab metrics for.
|
||||
#
|
||||
address = "host=localhost user=postgres sslmode=disable"
|
||||
|
||||
# A list of databases to pull metrics about. If not specified, metrics for all
|
||||
# databases are gathered.
|
||||
# databases = ["app_production", "testing"]
|
||||
`
|
||||
|
||||
func (p *Postgresql) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (p *Postgresql) Description() string {
|
||||
return "Read metrics from one or many postgresql servers"
|
||||
}
|
||||
|
||||
func (p *Postgresql) IgnoredColumns() map[string]bool {
|
||||
return ignoredColumns
|
||||
}
|
||||
|
||||
var localhost = "host=localhost sslmode=disable"
|
||||
|
||||
func (p *Postgresql) Gather(acc inputs.Accumulator) error {
|
||||
var query string
|
||||
|
||||
if p.Address == "" || p.Address == "localhost" {
|
||||
p.Address = localhost
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", p.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer db.Close()
|
||||
|
||||
if len(p.Databases) == 0 {
|
||||
query = `SELECT * FROM pg_stat_database`
|
||||
} else {
|
||||
query = fmt.Sprintf(`SELECT * FROM pg_stat_database WHERE datname IN ('%s')`,
|
||||
strings.Join(p.Databases, "','"))
|
||||
}
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
// grab the column information from the result
|
||||
p.OrderedColumns, err = rows.Columns()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
err = p.accRow(rows, acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
type scanner interface {
|
||||
Scan(dest ...interface{}) error
|
||||
}
|
||||
|
||||
func (p *Postgresql) accRow(row scanner, acc inputs.Accumulator) error {
|
||||
var columnVars []interface{}
|
||||
var dbname bytes.Buffer
|
||||
|
||||
// this is where we'll store the column name with its *interface{}
|
||||
columnMap := make(map[string]*interface{})
|
||||
|
||||
for _, column := range p.OrderedColumns {
|
||||
columnMap[column] = new(interface{})
|
||||
}
|
||||
|
||||
// populate the array of interface{} with the pointers in the right order
|
||||
for i := 0; i < len(columnMap); i++ {
|
||||
columnVars = append(columnVars, columnMap[p.OrderedColumns[i]])
|
||||
}
|
||||
|
||||
// deconstruct array of variables and send to Scan
|
||||
err := row.Scan(columnVars...)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// extract the database name from the column map
|
||||
dbnameChars := (*columnMap["datname"]).([]uint8)
|
||||
for i := 0; i < len(dbnameChars); i++ {
|
||||
dbname.WriteString(string(dbnameChars[i]))
|
||||
}
|
||||
|
||||
tags := map[string]string{"server": p.Address, "db": dbname.String()}
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
for col, val := range columnMap {
|
||||
_, ignore := ignoredColumns[col]
|
||||
if !ignore {
|
||||
fields[col] = *val
|
||||
}
|
||||
}
|
||||
acc.AddFields("postgresql", fields, tags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("postgresql", func() inputs.Input {
|
||||
return &Postgresql{}
|
||||
})
|
||||
}
|
||||
146
plugins/inputs/postgresql/postgresql_test.go
Normal file
146
plugins/inputs/postgresql/postgresql_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
p := &Postgresql{
|
||||
Address: fmt.Sprintf("host=%s user=postgres sslmode=disable",
|
||||
testutil.GetLocalHost()),
|
||||
Databases: []string{"postgres"},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := p.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
availableColumns := make(map[string]bool)
|
||||
for _, col := range p.OrderedColumns {
|
||||
availableColumns[col] = true
|
||||
}
|
||||
|
||||
intMetrics := []string{
|
||||
"xact_commit",
|
||||
"xact_rollback",
|
||||
"blks_read",
|
||||
"blks_hit",
|
||||
"tup_returned",
|
||||
"tup_fetched",
|
||||
"tup_inserted",
|
||||
"tup_updated",
|
||||
"tup_deleted",
|
||||
"conflicts",
|
||||
"temp_files",
|
||||
"temp_bytes",
|
||||
"deadlocks",
|
||||
"numbackends",
|
||||
}
|
||||
|
||||
floatMetrics := []string{
|
||||
"blk_read_time",
|
||||
"blk_write_time",
|
||||
}
|
||||
|
||||
metricsCounted := 0
|
||||
|
||||
for _, metric := range intMetrics {
|
||||
_, ok := availableColumns[metric]
|
||||
if ok {
|
||||
assert.True(t, acc.HasIntField("postgresql", metric))
|
||||
metricsCounted++
|
||||
}
|
||||
}
|
||||
|
||||
for _, metric := range floatMetrics {
|
||||
_, ok := availableColumns[metric]
|
||||
if ok {
|
||||
assert.True(t, acc.HasFloatField("postgresql", metric))
|
||||
metricsCounted++
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, metricsCounted > 0)
|
||||
assert.Equal(t, len(availableColumns)-len(p.IgnoredColumns()), metricsCounted)
|
||||
}
|
||||
|
||||
func TestPostgresqlTagsMetricsWithDatabaseName(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
p := &Postgresql{
|
||||
Address: fmt.Sprintf("host=%s user=postgres sslmode=disable",
|
||||
testutil.GetLocalHost()),
|
||||
Databases: []string{"postgres"},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := p.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
point, ok := acc.Get("postgresql")
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, "postgres", point.Tags["db"])
|
||||
}
|
||||
|
||||
func TestPostgresqlDefaultsToAllDatabases(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
p := &Postgresql{
|
||||
Address: fmt.Sprintf("host=%s user=postgres sslmode=disable",
|
||||
testutil.GetLocalHost()),
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := p.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
var found bool
|
||||
|
||||
for _, pnt := range acc.Points {
|
||||
if pnt.Measurement == "postgresql" {
|
||||
if pnt.Tags["db"] == "postgres" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
func TestPostgresqlIgnoresUnwantedColumns(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
p := &Postgresql{
|
||||
Address: fmt.Sprintf("host=%s user=postgres sslmode=disable",
|
||||
testutil.GetLocalHost()),
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := p.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
for col := range p.IgnoredColumns() {
|
||||
assert.False(t, acc.HasMeasurement(col))
|
||||
}
|
||||
}
|
||||
72
plugins/inputs/procstat/README.md
Normal file
72
plugins/inputs/procstat/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Telegraf plugin: procstat
|
||||
|
||||
#### Description
|
||||
|
||||
The procstat plugin can be used to monitor system resource usage by an
|
||||
individual process using their /proc data.
|
||||
|
||||
The plugin will tag processes by their PID and their process name.
|
||||
|
||||
Processes can be specified either by pid file or by executable name. Procstat
|
||||
plugin will use `pgrep` when executable name is provided to obtain the pid.
|
||||
Proctstas plugin will transmit IO, memory, cpu, file descriptor related
|
||||
measurements for every process specified. A prefix can be set to isolate
|
||||
individual process specific measurements.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
[procstat]
|
||||
|
||||
[[procstat.specifications]]
|
||||
exe = "influxd"
|
||||
prefix = "influxd"
|
||||
|
||||
[[procstat.specifications]]
|
||||
pid_file = "/var/run/lxc/dnsmasq.pid"
|
||||
```
|
||||
|
||||
The above configuration would result in output like:
|
||||
|
||||
```
|
||||
[...]
|
||||
> [name="dnsmasq" pid="44979"] procstat_cpu_user value=0.14
|
||||
> [name="dnsmasq" pid="44979"] procstat_cpu_system value=0.07
|
||||
[...]
|
||||
> [name="influxd" pid="34337"] procstat_influxd_cpu_user value=25.43
|
||||
> [name="influxd" pid="34337"] procstat_influxd_cpu_system value=21.82
|
||||
```
|
||||
|
||||
# Measurements
|
||||
Note: prefix can be set by the user, per process.
|
||||
|
||||
File descriptor related measurement names:
|
||||
- procstat_[prefix_]num_fds value=4
|
||||
|
||||
Context switch related measurement names:
|
||||
- procstat_[prefix_]voluntary_context_switches value=250
|
||||
- procstat_[prefix_]involuntary_context_switches value=0
|
||||
|
||||
I/O related measurement names:
|
||||
- procstat_[prefix_]read_count value=396
|
||||
- procstat_[prefix_]write_count value=1
|
||||
- procstat_[prefix_]read_bytes value=1019904
|
||||
- procstat_[prefix_]write_bytes value=1
|
||||
|
||||
CPU related measurement names:
|
||||
- procstat_[prefix_]cpu_user value=0
|
||||
- procstat_[prefix_]cpu_system value=0.01
|
||||
- procstat_[prefix_]cpu_idle value=0
|
||||
- procstat_[prefix_]cpu_nice value=0
|
||||
- procstat_[prefix_]cpu_iowait value=0
|
||||
- procstat_[prefix_]cpu_irq value=0
|
||||
- procstat_[prefix_]cpu_soft_irq value=0
|
||||
- procstat_[prefix_]cpu_soft_steal value=0
|
||||
- procstat_[prefix_]cpu_soft_stolen value=0
|
||||
- procstat_[prefix_]cpu_soft_guest value=0
|
||||
- procstat_[prefix_]cpu_soft_guest_nice value=0
|
||||
|
||||
Memory related measurement names:
|
||||
- procstat_[prefix_]memory_rss value=1777664
|
||||
- procstat_[prefix_]memory_vms value=24227840
|
||||
- procstat_[prefix_]memory_swap value=282624
|
||||
167
plugins/inputs/procstat/procstat.go
Normal file
167
plugins/inputs/procstat/procstat.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package procstat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/process"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type Procstat struct {
|
||||
PidFile string `toml:"pid_file"`
|
||||
Exe string
|
||||
Pattern string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func NewProcstat() *Procstat {
|
||||
return &Procstat{}
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# Must specify one of: pid_file, exe, or pattern
|
||||
# PID file to monitor process
|
||||
pid_file = "/var/run/nginx.pid"
|
||||
# executable name (ie, pgrep <exe>)
|
||||
# exe = "nginx"
|
||||
# pattern as argument for pgrep (ie, pgrep -f <pattern>)
|
||||
# pattern = "nginx"
|
||||
|
||||
# Field name prefix
|
||||
prefix = ""
|
||||
`
|
||||
|
||||
func (_ *Procstat) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (_ *Procstat) Description() string {
|
||||
return "Monitor process cpu and memory usage"
|
||||
}
|
||||
|
||||
func (p *Procstat) Gather(acc inputs.Accumulator) error {
|
||||
procs, err := p.createProcesses()
|
||||
if err != nil {
|
||||
log.Printf("Error: procstat getting process, exe: [%s] pidfile: [%s] pattern: [%s] %s",
|
||||
p.Exe, p.PidFile, p.Pattern, err.Error())
|
||||
} else {
|
||||
for _, proc := range procs {
|
||||
p := NewSpecProcessor(p.Prefix, acc, proc)
|
||||
p.pushMetrics()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Procstat) createProcesses() ([]*process.Process, error) {
|
||||
var out []*process.Process
|
||||
var errstring string
|
||||
var outerr error
|
||||
|
||||
pids, err := p.getAllPids()
|
||||
if err != nil {
|
||||
errstring += err.Error() + " "
|
||||
}
|
||||
|
||||
for _, pid := range pids {
|
||||
p, err := process.NewProcess(int32(pid))
|
||||
if err == nil {
|
||||
out = append(out, p)
|
||||
} else {
|
||||
errstring += err.Error() + " "
|
||||
}
|
||||
}
|
||||
|
||||
if errstring != "" {
|
||||
outerr = fmt.Errorf("%s", errstring)
|
||||
}
|
||||
|
||||
return out, outerr
|
||||
}
|
||||
|
||||
func (p *Procstat) getAllPids() ([]int32, error) {
|
||||
var pids []int32
|
||||
var err error
|
||||
|
||||
if p.PidFile != "" {
|
||||
pids, err = pidsFromFile(p.PidFile)
|
||||
} else if p.Exe != "" {
|
||||
pids, err = pidsFromExe(p.Exe)
|
||||
} else if p.Pattern != "" {
|
||||
pids, err = pidsFromPattern(p.Pattern)
|
||||
} else {
|
||||
err = fmt.Errorf("Either exe, pid_file or pattern has to be specified")
|
||||
}
|
||||
|
||||
return pids, err
|
||||
}
|
||||
|
||||
func pidsFromFile(file string) ([]int32, error) {
|
||||
var out []int32
|
||||
var outerr error
|
||||
pidString, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
outerr = fmt.Errorf("Failed to read pidfile '%s'. Error: '%s'", file, err)
|
||||
} else {
|
||||
pid, err := strconv.Atoi(strings.TrimSpace(string(pidString)))
|
||||
if err != nil {
|
||||
outerr = err
|
||||
} else {
|
||||
out = append(out, int32(pid))
|
||||
}
|
||||
}
|
||||
return out, outerr
|
||||
}
|
||||
|
||||
func pidsFromExe(exe string) ([]int32, error) {
|
||||
var out []int32
|
||||
var outerr error
|
||||
pgrep, err := exec.Command("pgrep", exe).Output()
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("Failed to execute pgrep. Error: '%s'", err)
|
||||
} else {
|
||||
pids := strings.Fields(string(pgrep))
|
||||
for _, pid := range pids {
|
||||
ipid, err := strconv.Atoi(pid)
|
||||
if err == nil {
|
||||
out = append(out, int32(ipid))
|
||||
} else {
|
||||
outerr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, outerr
|
||||
}
|
||||
|
||||
func pidsFromPattern(pattern string) ([]int32, error) {
|
||||
var out []int32
|
||||
var outerr error
|
||||
pgrep, err := exec.Command("pgrep", "-f", pattern).Output()
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("Failed to execute pgrep. Error: '%s'", err)
|
||||
} else {
|
||||
pids := strings.Fields(string(pgrep))
|
||||
for _, pid := range pids {
|
||||
ipid, err := strconv.Atoi(pid)
|
||||
if err == nil {
|
||||
out = append(out, int32(ipid))
|
||||
} else {
|
||||
outerr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, outerr
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("procstat", func() inputs.Input {
|
||||
return NewProcstat()
|
||||
})
|
||||
}
|
||||
30
plugins/inputs/procstat/procstat_test.go
Normal file
30
plugins/inputs/procstat/procstat_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package procstat
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
)
|
||||
|
||||
func TestGather(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
pid := os.Getpid()
|
||||
file, err := ioutil.TempFile(os.TempDir(), "telegraf")
|
||||
require.NoError(t, err)
|
||||
file.Write([]byte(strconv.Itoa(pid)))
|
||||
file.Close()
|
||||
defer os.Remove(file.Name())
|
||||
p := Procstat{
|
||||
PidFile: file.Name(),
|
||||
Prefix: "foo",
|
||||
}
|
||||
p.Gather(&acc)
|
||||
assert.True(t, acc.HasFloatField("procstat", "foo_cpu_time_user"))
|
||||
assert.True(t, acc.HasUIntField("procstat", "foo_memory_vms"))
|
||||
}
|
||||
133
plugins/inputs/procstat/spec_processor.go
Normal file
133
plugins/inputs/procstat/spec_processor.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package procstat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/shirou/gopsutil/process"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type SpecProcessor struct {
|
||||
Prefix string
|
||||
tags map[string]string
|
||||
fields map[string]interface{}
|
||||
acc inputs.Accumulator
|
||||
proc *process.Process
|
||||
}
|
||||
|
||||
func (p *SpecProcessor) add(metric string, value interface{}) {
|
||||
var mname string
|
||||
if p.Prefix == "" {
|
||||
mname = metric
|
||||
} else {
|
||||
mname = p.Prefix + "_" + metric
|
||||
}
|
||||
p.fields[mname] = value
|
||||
}
|
||||
|
||||
func (p *SpecProcessor) flush() {
|
||||
p.acc.AddFields("procstat", p.fields, p.tags)
|
||||
p.fields = make(map[string]interface{})
|
||||
}
|
||||
|
||||
func NewSpecProcessor(
|
||||
prefix string,
|
||||
acc inputs.Accumulator,
|
||||
p *process.Process,
|
||||
) *SpecProcessor {
|
||||
tags := make(map[string]string)
|
||||
tags["pid"] = fmt.Sprintf("%v", p.Pid)
|
||||
if name, err := p.Name(); err == nil {
|
||||
tags["name"] = name
|
||||
}
|
||||
return &SpecProcessor{
|
||||
Prefix: prefix,
|
||||
tags: tags,
|
||||
fields: make(map[string]interface{}),
|
||||
acc: acc,
|
||||
proc: p,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SpecProcessor) pushMetrics() {
|
||||
if err := p.pushFDStats(); err != nil {
|
||||
log.Printf("procstat, fd stats not available: %s", err.Error())
|
||||
}
|
||||
if err := p.pushCtxStats(); err != nil {
|
||||
log.Printf("procstat, ctx stats not available: %s", err.Error())
|
||||
}
|
||||
if err := p.pushIOStats(); err != nil {
|
||||
log.Printf("procstat, io stats not available: %s", err.Error())
|
||||
}
|
||||
if err := p.pushCPUStats(); err != nil {
|
||||
log.Printf("procstat, cpu stats not available: %s", err.Error())
|
||||
}
|
||||
if err := p.pushMemoryStats(); err != nil {
|
||||
log.Printf("procstat, mem stats not available: %s", err.Error())
|
||||
}
|
||||
p.flush()
|
||||
}
|
||||
|
||||
func (p *SpecProcessor) pushFDStats() error {
|
||||
fds, err := p.proc.NumFDs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("NumFD error: %s\n", err)
|
||||
}
|
||||
p.add("num_fds", fds)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SpecProcessor) pushCtxStats() error {
|
||||
ctx, err := p.proc.NumCtxSwitches()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ContextSwitch error: %s\n", err)
|
||||
}
|
||||
p.add("voluntary_context_switches", ctx.Voluntary)
|
||||
p.add("involuntary_context_switches", ctx.Involuntary)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SpecProcessor) pushIOStats() error {
|
||||
io, err := p.proc.IOCounters()
|
||||
if err != nil {
|
||||
return fmt.Errorf("IOCounters error: %s\n", err)
|
||||
}
|
||||
p.add("read_count", io.ReadCount)
|
||||
p.add("write_count", io.WriteCount)
|
||||
p.add("read_bytes", io.ReadBytes)
|
||||
p.add("write_bytes", io.WriteCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SpecProcessor) pushCPUStats() error {
|
||||
cpu_time, err := p.proc.CPUTimes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.add("cpu_time_user", cpu_time.User)
|
||||
p.add("cpu_time_system", cpu_time.System)
|
||||
p.add("cpu_time_idle", cpu_time.Idle)
|
||||
p.add("cpu_time_nice", cpu_time.Nice)
|
||||
p.add("cpu_time_iowait", cpu_time.Iowait)
|
||||
p.add("cpu_time_irq", cpu_time.Irq)
|
||||
p.add("cpu_time_soft_irq", cpu_time.Softirq)
|
||||
p.add("cpu_time_soft_steal", cpu_time.Steal)
|
||||
p.add("cpu_time_soft_stolen", cpu_time.Stolen)
|
||||
p.add("cpu_time_soft_guest", cpu_time.Guest)
|
||||
p.add("cpu_time_soft_guest_nice", cpu_time.GuestNice)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SpecProcessor) pushMemoryStats() error {
|
||||
mem, err := p.proc.MemoryInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.add("memory_rss", mem.RSS)
|
||||
p.add("memory_vms", mem.VMS)
|
||||
p.add("memory_swap", mem.Swap)
|
||||
return nil
|
||||
}
|
||||
103
plugins/inputs/prometheus/prometheus.go
Normal file
103
plugins/inputs/prometheus/prometheus.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
"github.com/prometheus/common/expfmt"
|
||||
"github.com/prometheus/common/model"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Prometheus struct {
|
||||
Urls []string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of urls to scrape metrics from.
|
||||
urls = ["http://localhost:9100/metrics"]
|
||||
`
|
||||
|
||||
func (r *Prometheus) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (r *Prometheus) Description() string {
|
||||
return "Read metrics from one or many prometheus clients"
|
||||
}
|
||||
|
||||
var ErrProtocolError = errors.New("prometheus protocol error")
|
||||
|
||||
// Reads stats from all configured servers accumulates stats.
|
||||
// Returns one of the errors encountered while gather stats (if any).
|
||||
func (g *Prometheus) Gather(acc inputs.Accumulator) error {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var outerr error
|
||||
|
||||
for _, serv := range g.Urls {
|
||||
wg.Add(1)
|
||||
go func(serv string) {
|
||||
defer wg.Done()
|
||||
outerr = g.gatherURL(serv, acc)
|
||||
}(serv)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
func (g *Prometheus) gatherURL(url string, acc inputs.Accumulator) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error making HTTP request to %s: %s", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%s returned HTTP status %s", url, resp.Status)
|
||||
}
|
||||
format := expfmt.ResponseFormat(resp.Header)
|
||||
|
||||
decoder := expfmt.NewDecoder(resp.Body, format)
|
||||
|
||||
options := &expfmt.DecodeOptions{
|
||||
Timestamp: model.Now(),
|
||||
}
|
||||
sampleDecoder := &expfmt.SampleDecoder{
|
||||
Dec: decoder,
|
||||
Opts: options,
|
||||
}
|
||||
|
||||
for {
|
||||
var samples model.Vector
|
||||
err := sampleDecoder.Decode(&samples)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error getting processing samples for %s: %s",
|
||||
url, err)
|
||||
}
|
||||
for _, sample := range samples {
|
||||
tags := make(map[string]string)
|
||||
for key, value := range sample.Metric {
|
||||
if key == model.MetricNameLabel {
|
||||
continue
|
||||
}
|
||||
tags[string(key)] = string(value)
|
||||
}
|
||||
acc.Add("prometheus_"+string(sample.Metric[model.MetricNameLabel]),
|
||||
float64(sample.Value), tags)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("prometheus", func() inputs.Input {
|
||||
return &Prometheus{}
|
||||
})
|
||||
}
|
||||
55
plugins/inputs/prometheus/prometheus_test.go
Normal file
55
plugins/inputs/prometheus/prometheus_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const sampleTextFormat = `# HELP go_gc_duration_seconds A summary of the GC invocation durations.
|
||||
# TYPE go_gc_duration_seconds summary
|
||||
go_gc_duration_seconds{quantile="0"} 0.00010425500000000001
|
||||
go_gc_duration_seconds{quantile="0.25"} 0.000139108
|
||||
go_gc_duration_seconds{quantile="0.5"} 0.00015749400000000002
|
||||
go_gc_duration_seconds{quantile="0.75"} 0.000331463
|
||||
go_gc_duration_seconds{quantile="1"} 0.000667154
|
||||
go_gc_duration_seconds_sum 0.0018183950000000002
|
||||
go_gc_duration_seconds_count 7
|
||||
# HELP go_goroutines Number of goroutines that currently exist.
|
||||
# TYPE go_goroutines gauge
|
||||
go_goroutines 15
|
||||
`
|
||||
|
||||
func TestPrometheusGeneratesMetrics(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, sampleTextFormat)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
p := &Prometheus{
|
||||
Urls: []string{ts.URL},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := p.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []struct {
|
||||
name string
|
||||
value float64
|
||||
tags map[string]string
|
||||
}{
|
||||
{"prometheus_go_gc_duration_seconds_count", 7, map[string]string{}},
|
||||
{"prometheus_go_goroutines", 15, map[string]string{}},
|
||||
}
|
||||
|
||||
for _, e := range expected {
|
||||
assert.True(t, acc.HasFloatField(e.name, "value"))
|
||||
}
|
||||
}
|
||||
132
plugins/inputs/puppetagent/README.md
Normal file
132
plugins/inputs/puppetagent/README.md
Normal file
@@ -0,0 +1,132 @@
|
||||
## Telegraf Plugin: PuppetAgent
|
||||
|
||||
#### Description
|
||||
|
||||
The puppetagent plugin collects variables outputted from the 'last_run_summary.yaml' file
|
||||
usually located in `/var/lib/puppet/state/`
|
||||
[PuppetAgent Runs](https://puppetlabs.com/blog/puppet-monitoring-how-to-monitor-the-success-or-failure-of-puppet-runs).
|
||||
|
||||
```
|
||||
cat /var/lib/puppet/state/last_run_summary.yaml
|
||||
|
||||
---
|
||||
events:
|
||||
failure: 0
|
||||
total: 0
|
||||
success: 0
|
||||
resources:
|
||||
failed: 0
|
||||
scheduled: 0
|
||||
changed: 0
|
||||
skipped: 0
|
||||
total: 109
|
||||
failed_to_restart: 0
|
||||
restarted: 0
|
||||
out_of_sync: 0
|
||||
changes:
|
||||
total: 0
|
||||
time:
|
||||
user: 0.004331
|
||||
schedule: 0.001123
|
||||
filebucket: 0.000353
|
||||
file: 0.441472
|
||||
exec: 0.508123
|
||||
anchor: 0.000555
|
||||
yumrepo: 0.006989
|
||||
ssh_authorized_key: 0.000764
|
||||
service: 1.807795
|
||||
package: 1.325788
|
||||
total: 8.85354707064819
|
||||
config_retrieval: 4.75567007064819
|
||||
last_run: 1444936531
|
||||
cron: 0.000584
|
||||
version:
|
||||
config: 1444936521
|
||||
puppet: "3.7.5"
|
||||
```
|
||||
|
||||
```
|
||||
jcross@pit-devops-02 ~ >sudo ./telegraf_linux_amd64 -filter puppetagent -config tele.conf -test
|
||||
* Plugin: puppetagent, Collection 1
|
||||
> [] puppetagent_events_failure value=0
|
||||
> [] puppetagent_events_total value=0
|
||||
> [] puppetagent_events_success value=0
|
||||
> [] puppetagent_resources_failed value=0
|
||||
> [] puppetagent_resources_scheduled value=0
|
||||
> [] puppetagent_resources_changed value=0
|
||||
> [] puppetagent_resources_skipped value=0
|
||||
> [] puppetagent_resources_total value=109
|
||||
> [] puppetagent_resources_failedtorestart value=0
|
||||
> [] puppetagent_resources_restarted value=0
|
||||
> [] puppetagent_resources_outofsync value=0
|
||||
> [] puppetagent_changes_total value=0
|
||||
> [] puppetagent_time_user value=0.00393
|
||||
> [] puppetagent_time_schedule value=0.001234
|
||||
> [] puppetagent_time_filebucket value=0.000244
|
||||
> [] puppetagent_time_file value=0.587734
|
||||
> [] puppetagent_time_exec value=0.389584
|
||||
> [] puppetagent_time_anchor value=0.000399
|
||||
> [] puppetagent_time_sshauthorizedkey value=0.000655
|
||||
> [] puppetagent_time_service value=0
|
||||
> [] puppetagent_time_package value=1.297537
|
||||
> [] puppetagent_time_total value=9.45297606225586
|
||||
> [] puppetagent_time_configretrieval value=5.89822006225586
|
||||
> [] puppetagent_time_lastrun value=1444940131
|
||||
> [] puppetagent_time_cron value=0.000646
|
||||
> [] puppetagent_version_config value=1444940121
|
||||
> [] puppetagent_version_puppet value=3.7.5
|
||||
```
|
||||
|
||||
## Measurements:
|
||||
#### PuppetAgent int64 measurements:
|
||||
|
||||
Meta:
|
||||
- units: int64
|
||||
- tags: ``
|
||||
|
||||
Measurement names:
|
||||
- puppetagent_events_failure
|
||||
- puppetagent_events_total
|
||||
- puppetagent_events_success
|
||||
- puppetagent_resources_failed
|
||||
- puppetagent_resources_scheduled
|
||||
- puppetagent_resources_changed
|
||||
- puppetagent_resources_skipped
|
||||
- puppetagent_resources_total
|
||||
- puppetagent_resources_failedtorestart
|
||||
- puppetagent_resources_restarted
|
||||
- puppetagent_resources_outofsync
|
||||
- puppetagent_changes_total
|
||||
- puppetagent_time_service
|
||||
- puppetagent_time_lastrun
|
||||
- puppetagent_version_config
|
||||
|
||||
#### PuppetAgent float64 measurements:
|
||||
|
||||
Meta:
|
||||
- units: float64
|
||||
- tags: ``
|
||||
|
||||
Measurement names:
|
||||
- puppetagent_time_user
|
||||
- puppetagent_time_schedule
|
||||
- puppetagent_time_filebucket
|
||||
- puppetagent_time_file
|
||||
- puppetagent_time_exec
|
||||
- puppetagent_time_anchor
|
||||
- puppetagent_time_sshauthorizedkey
|
||||
- puppetagent_time_package
|
||||
- puppetagent_time_total
|
||||
- puppetagent_time_configretrieval
|
||||
- puppetagent_time_lastrun
|
||||
- puppetagent_time_cron
|
||||
- puppetagent_version_config
|
||||
|
||||
#### PuppetAgent string measurements:
|
||||
|
||||
Meta:
|
||||
- units: string
|
||||
- tags: ``
|
||||
|
||||
Measurement names:
|
||||
- puppetagent_version_puppet
|
||||
34
plugins/inputs/puppetagent/last_run_summary.yaml
Normal file
34
plugins/inputs/puppetagent/last_run_summary.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
events:
|
||||
failure: 0
|
||||
total: 0
|
||||
success: 0
|
||||
resources:
|
||||
failed: 0
|
||||
scheduled: 0
|
||||
changed: 0
|
||||
skipped: 0
|
||||
total: 109
|
||||
failed_to_restart: 0
|
||||
restarted: 0
|
||||
out_of_sync: 0
|
||||
changes:
|
||||
total: 0
|
||||
time:
|
||||
user: 0.004331
|
||||
schedule: 0.001123
|
||||
filebucket: 0.000353
|
||||
file: 0.441472
|
||||
exec: 0.508123
|
||||
anchor: 0.000555
|
||||
yumrepo: 0.006989
|
||||
ssh_authorized_key: 0.000764
|
||||
service: 1.807795
|
||||
package: 1.325788
|
||||
total: 8.85354707064819
|
||||
config_retrieval: 4.75567007064819
|
||||
last_run: 1444936531
|
||||
cron: 0.000584
|
||||
version:
|
||||
config: 1444936521
|
||||
puppet: "3.7.5"
|
||||
137
plugins/inputs/puppetagent/puppetagent.go
Normal file
137
plugins/inputs/puppetagent/puppetagent.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package puppetagent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
// PuppetAgent is a PuppetAgent plugin
|
||||
type PuppetAgent struct {
|
||||
Location string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# Location of puppet last run summary file
|
||||
location = "/var/lib/puppet/state/last_run_summary.yaml"
|
||||
`
|
||||
|
||||
type State struct {
|
||||
Events event
|
||||
Resources resource
|
||||
Changes change
|
||||
Time time
|
||||
Version version
|
||||
}
|
||||
|
||||
type event struct {
|
||||
Failure int64 `yaml:"failure"`
|
||||
Total int64 `yaml:"total"`
|
||||
Success int64 `yaml:"success"`
|
||||
}
|
||||
|
||||
type resource struct {
|
||||
Failed int64 `yaml:"failed"`
|
||||
Scheduled int64 `yaml:"scheduled"`
|
||||
Changed int64 `yaml:"changed"`
|
||||
Skipped int64 `yaml:"skipped"`
|
||||
Total int64 `yaml:"total"`
|
||||
FailedToRestart int64 `yaml:"failed_to_restart"`
|
||||
Restarted int64 `yaml:"restarted"`
|
||||
OutOfSync int64 `yaml:"out_of_sync"`
|
||||
}
|
||||
|
||||
type change struct {
|
||||
Total int64 `yaml:"total"`
|
||||
}
|
||||
|
||||
type time struct {
|
||||
User float64 `yaml:"user"`
|
||||
Schedule float64 `yaml:"schedule"`
|
||||
FileBucket float64 `yaml:"filebucket"`
|
||||
File float64 `yaml:"file"`
|
||||
Exec float64 `yaml:"exec"`
|
||||
Anchor float64 `yaml:"anchor"`
|
||||
SSHAuthorizedKey float64 `yaml:"ssh_authorized_key"`
|
||||
Service float64 `yaml:"service"`
|
||||
Package float64 `yaml:"package"`
|
||||
Total float64 `yaml:"total"`
|
||||
ConfigRetrieval float64 `yaml:"config_retrieval"`
|
||||
LastRun int64 `yaml:"last_run"`
|
||||
Cron float64 `yaml:"cron"`
|
||||
}
|
||||
|
||||
type version struct {
|
||||
Config int64 `yaml:"config"`
|
||||
Puppet string `yaml:"puppet"`
|
||||
}
|
||||
|
||||
// SampleConfig returns sample configuration message
|
||||
func (pa *PuppetAgent) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
// Description returns description of PuppetAgent plugin
|
||||
func (pa *PuppetAgent) Description() string {
|
||||
return `Reads last_run_summary.yaml file and converts to measurments`
|
||||
}
|
||||
|
||||
// Gather reads stats from all configured servers accumulates stats
|
||||
func (pa *PuppetAgent) Gather(acc inputs.Accumulator) error {
|
||||
|
||||
if len(pa.Location) == 0 {
|
||||
pa.Location = "/var/lib/puppet/state/last_run_summary.yaml"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(pa.Location); err != nil {
|
||||
return fmt.Errorf("%s", err)
|
||||
}
|
||||
|
||||
fh, err := ioutil.ReadFile(pa.Location)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s", err)
|
||||
}
|
||||
|
||||
var puppetState State
|
||||
|
||||
err = yaml.Unmarshal(fh, &puppetState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s", err)
|
||||
}
|
||||
|
||||
tags := map[string]string{"location": pa.Location}
|
||||
structPrinter(&puppetState, acc, tags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func structPrinter(s *State, acc inputs.Accumulator, tags map[string]string) {
|
||||
e := reflect.ValueOf(s).Elem()
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
for tLevelFNum := 0; tLevelFNum < e.NumField(); tLevelFNum++ {
|
||||
name := e.Type().Field(tLevelFNum).Name
|
||||
nameNumField := e.FieldByName(name).NumField()
|
||||
|
||||
for sLevelFNum := 0; sLevelFNum < nameNumField; sLevelFNum++ {
|
||||
sName := e.FieldByName(name).Type().Field(sLevelFNum).Name
|
||||
sValue := e.FieldByName(name).Field(sLevelFNum).Interface()
|
||||
|
||||
lname := strings.ToLower(name)
|
||||
lsName := strings.ToLower(sName)
|
||||
fields[fmt.Sprintf("%s_%s", lname, lsName)] = sValue
|
||||
}
|
||||
}
|
||||
acc.AddFields("puppetagent", fields, tags)
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("puppetagent", func() inputs.Input {
|
||||
return &PuppetAgent{}
|
||||
})
|
||||
}
|
||||
48
plugins/inputs/puppetagent/puppetagent_test.go
Normal file
48
plugins/inputs/puppetagent/puppetagent_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package puppetagent
|
||||
|
||||
import (
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGather(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
|
||||
pa := PuppetAgent{
|
||||
Location: "last_run_summary.yaml",
|
||||
}
|
||||
pa.Gather(&acc)
|
||||
|
||||
tags := map[string]string{"location": "last_run_summary.yaml"}
|
||||
fields := map[string]interface{}{
|
||||
"events_failure": int64(0),
|
||||
"events_total": int64(0),
|
||||
"events_success": int64(0),
|
||||
"resources_failed": int64(0),
|
||||
"resources_scheduled": int64(0),
|
||||
"resources_changed": int64(0),
|
||||
"resources_skipped": int64(0),
|
||||
"resources_total": int64(109),
|
||||
"resources_failedtorestart": int64(0),
|
||||
"resources_restarted": int64(0),
|
||||
"resources_outofsync": int64(0),
|
||||
"changes_total": int64(0),
|
||||
"time_lastrun": int64(1444936531),
|
||||
"version_config": int64(1444936521),
|
||||
"time_user": float64(0.004331),
|
||||
"time_schedule": float64(0.001123),
|
||||
"time_filebucket": float64(0.000353),
|
||||
"time_file": float64(0.441472),
|
||||
"time_exec": float64(0.508123),
|
||||
"time_anchor": float64(0.000555),
|
||||
"time_sshauthorizedkey": float64(0.000764),
|
||||
"time_service": float64(1.807795),
|
||||
"time_package": float64(1.325788),
|
||||
"time_total": float64(8.85354707064819),
|
||||
"time_configretrieval": float64(4.75567007064819),
|
||||
"time_cron": float64(0.000584),
|
||||
"version_puppet": "3.7.5",
|
||||
}
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "puppetagent", fields, tags)
|
||||
}
|
||||
326
plugins/inputs/rabbitmq/rabbitmq.go
Normal file
326
plugins/inputs/rabbitmq/rabbitmq.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
const DefaultUsername = "guest"
|
||||
const DefaultPassword = "guest"
|
||||
const DefaultURL = "http://localhost:15672"
|
||||
|
||||
type RabbitMQ struct {
|
||||
URL string
|
||||
Name string
|
||||
Username string
|
||||
Password string
|
||||
Nodes []string
|
||||
Queues []string
|
||||
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type OverviewResponse struct {
|
||||
MessageStats *MessageStats `json:"message_stats"`
|
||||
ObjectTotals *ObjectTotals `json:"object_totals"`
|
||||
QueueTotals *QueueTotals `json:"queue_totals"`
|
||||
}
|
||||
|
||||
type Details struct {
|
||||
Rate float64
|
||||
}
|
||||
|
||||
type MessageStats struct {
|
||||
Ack int64
|
||||
AckDetails Details `json:"ack_details"`
|
||||
Deliver int64
|
||||
DeliverDetails Details `json:"deliver_details"`
|
||||
DeliverGet int64
|
||||
DeliverGetDetails Details `json:"deliver_get_details"`
|
||||
Publish int64
|
||||
PublishDetails Details `json:"publish_details"`
|
||||
Redeliver int64
|
||||
RedeliverDetails Details `json:"redeliver_details"`
|
||||
}
|
||||
|
||||
type ObjectTotals struct {
|
||||
Channels int64
|
||||
Connections int64
|
||||
Consumers int64
|
||||
Exchanges int64
|
||||
Queues int64
|
||||
}
|
||||
|
||||
type QueueTotals struct {
|
||||
Messages int64
|
||||
MessagesReady int64 `json:"messages_ready"`
|
||||
MessagesUnacknowledged int64 `json:"messages_unacknowledged"`
|
||||
}
|
||||
|
||||
type Queue struct {
|
||||
QueueTotals // just to not repeat the same code
|
||||
MessageStats `json:"message_stats"`
|
||||
Memory int64
|
||||
Consumers int64
|
||||
ConsumerUtilisation float64 `json:"consumer_utilisation"`
|
||||
Name string
|
||||
Node string
|
||||
Vhost string
|
||||
Durable bool
|
||||
AutoDelete bool `json:"auto_delete"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Name string
|
||||
|
||||
DiskFree int64 `json:"disk_free"`
|
||||
DiskFreeLimit int64 `json:"disk_free_limit"`
|
||||
FdTotal int64 `json:"fd_total"`
|
||||
FdUsed int64 `json:"fd_used"`
|
||||
MemLimit int64 `json:"mem_limit"`
|
||||
MemUsed int64 `json:"mem_used"`
|
||||
ProcTotal int64 `json:"proc_total"`
|
||||
ProcUsed int64 `json:"proc_used"`
|
||||
RunQueue int64 `json:"run_queue"`
|
||||
SocketsTotal int64 `json:"sockets_total"`
|
||||
SocketsUsed int64 `json:"sockets_used"`
|
||||
}
|
||||
|
||||
type gatherFunc func(r *RabbitMQ, acc inputs.Accumulator, errChan chan error)
|
||||
|
||||
var gatherFunctions = []gatherFunc{gatherOverview, gatherNodes, gatherQueues}
|
||||
|
||||
var sampleConfig = `
|
||||
url = "http://localhost:15672" # required
|
||||
# name = "rmq-server-1" # optional tag
|
||||
# username = "guest"
|
||||
# password = "guest"
|
||||
|
||||
# A list of nodes to pull metrics about. If not specified, metrics for
|
||||
# all nodes are gathered.
|
||||
# nodes = ["rabbit@node1", "rabbit@node2"]
|
||||
`
|
||||
|
||||
func (r *RabbitMQ) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (r *RabbitMQ) Description() string {
|
||||
return "Read metrics from one or many RabbitMQ servers via the management API"
|
||||
}
|
||||
|
||||
func (r *RabbitMQ) Gather(acc inputs.Accumulator) error {
|
||||
if r.Client == nil {
|
||||
r.Client = &http.Client{}
|
||||
}
|
||||
|
||||
var errChan = make(chan error, len(gatherFunctions))
|
||||
|
||||
for _, f := range gatherFunctions {
|
||||
go f(r, acc, errChan)
|
||||
}
|
||||
|
||||
for i := 1; i <= len(gatherFunctions); i++ {
|
||||
err := <-errChan
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RabbitMQ) requestJSON(u string, target interface{}) error {
|
||||
u = fmt.Sprintf("%s%s", r.URL, u)
|
||||
|
||||
req, err := http.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
username := r.Username
|
||||
if username == "" {
|
||||
username = DefaultUsername
|
||||
}
|
||||
|
||||
password := r.Password
|
||||
if password == "" {
|
||||
password = DefaultPassword
|
||||
}
|
||||
|
||||
req.SetBasicAuth(username, password)
|
||||
|
||||
resp, err := r.Client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
json.NewDecoder(resp.Body).Decode(target)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gatherOverview(r *RabbitMQ, acc inputs.Accumulator, errChan chan error) {
|
||||
overview := &OverviewResponse{}
|
||||
|
||||
err := r.requestJSON("/api/overview", &overview)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
if overview.QueueTotals == nil || overview.ObjectTotals == nil || overview.MessageStats == nil {
|
||||
errChan <- fmt.Errorf("Wrong answer from rabbitmq. Probably auth issue")
|
||||
return
|
||||
}
|
||||
|
||||
tags := map[string]string{"url": r.URL}
|
||||
if r.Name != "" {
|
||||
tags["name"] = r.Name
|
||||
}
|
||||
fields := map[string]interface{}{
|
||||
"messages": overview.QueueTotals.Messages,
|
||||
"messages_ready": overview.QueueTotals.MessagesReady,
|
||||
"messages_unacked": overview.QueueTotals.MessagesUnacknowledged,
|
||||
"channels": overview.ObjectTotals.Channels,
|
||||
"connections": overview.ObjectTotals.Connections,
|
||||
"consumers": overview.ObjectTotals.Consumers,
|
||||
"exchanges": overview.ObjectTotals.Exchanges,
|
||||
"queues": overview.ObjectTotals.Queues,
|
||||
"messages_acked": overview.MessageStats.Ack,
|
||||
"messages_delivered": overview.MessageStats.Deliver,
|
||||
"messages_published": overview.MessageStats.Publish,
|
||||
}
|
||||
acc.AddFields("rabbitmq_overview", fields, tags)
|
||||
|
||||
errChan <- nil
|
||||
}
|
||||
|
||||
func gatherNodes(r *RabbitMQ, acc inputs.Accumulator, errChan chan error) {
|
||||
nodes := make([]Node, 0)
|
||||
// Gather information about nodes
|
||||
err := r.requestJSON("/api/nodes", &nodes)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
for _, node := range nodes {
|
||||
if !r.shouldGatherNode(node) {
|
||||
continue
|
||||
}
|
||||
|
||||
tags := map[string]string{"url": r.URL}
|
||||
tags["node"] = node.Name
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"disk_free": node.DiskFree,
|
||||
"disk_free_limit": node.DiskFreeLimit,
|
||||
"fd_total": node.FdTotal,
|
||||
"fd_used": node.FdUsed,
|
||||
"mem_limit": node.MemLimit,
|
||||
"mem_used": node.MemUsed,
|
||||
"proc_total": node.ProcTotal,
|
||||
"proc_used": node.ProcUsed,
|
||||
"run_queue": node.RunQueue,
|
||||
"sockets_total": node.SocketsTotal,
|
||||
"sockets_used": node.SocketsUsed,
|
||||
}
|
||||
acc.AddFields("rabbitmq_node", fields, tags, now)
|
||||
}
|
||||
|
||||
errChan <- nil
|
||||
}
|
||||
|
||||
func gatherQueues(r *RabbitMQ, acc inputs.Accumulator, errChan chan error) {
|
||||
// Gather information about queues
|
||||
queues := make([]Queue, 0)
|
||||
err := r.requestJSON("/api/queues", &queues)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, queue := range queues {
|
||||
if !r.shouldGatherQueue(queue) {
|
||||
continue
|
||||
}
|
||||
tags := map[string]string{
|
||||
"url": r.URL,
|
||||
"queue": queue.Name,
|
||||
"vhost": queue.Vhost,
|
||||
"node": queue.Node,
|
||||
"durable": strconv.FormatBool(queue.Durable),
|
||||
"auto_delete": strconv.FormatBool(queue.AutoDelete),
|
||||
}
|
||||
|
||||
acc.AddFields(
|
||||
"rabbitmq_queue",
|
||||
map[string]interface{}{
|
||||
// common information
|
||||
"consumers": queue.Consumers,
|
||||
"consumer_utilisation": queue.ConsumerUtilisation,
|
||||
"memory": queue.Memory,
|
||||
// messages information
|
||||
"messages": queue.Messages,
|
||||
"messages_ready": queue.MessagesReady,
|
||||
"messages_unack": queue.MessagesUnacknowledged,
|
||||
"messages_ack": queue.MessageStats.Ack,
|
||||
"messages_ack_rate": queue.MessageStats.AckDetails.Rate,
|
||||
"messages_deliver": queue.MessageStats.Deliver,
|
||||
"messages_deliver_rate": queue.MessageStats.DeliverDetails.Rate,
|
||||
"messages_deliver_get": queue.MessageStats.DeliverGet,
|
||||
"messages_deliver_get_rate": queue.MessageStats.DeliverGetDetails.Rate,
|
||||
"messages_publish": queue.MessageStats.Publish,
|
||||
"messages_publish_rate": queue.MessageStats.PublishDetails.Rate,
|
||||
"messages_redeliver": queue.MessageStats.Redeliver,
|
||||
"messages_redeliver_rate": queue.MessageStats.RedeliverDetails.Rate,
|
||||
},
|
||||
tags,
|
||||
)
|
||||
}
|
||||
|
||||
errChan <- nil
|
||||
}
|
||||
|
||||
func (r *RabbitMQ) shouldGatherNode(node Node) bool {
|
||||
if len(r.Nodes) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, name := range r.Nodes {
|
||||
if name == node.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RabbitMQ) shouldGatherQueue(queue Queue) bool {
|
||||
if len(r.Queues) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, name := range r.Queues {
|
||||
if name == queue.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("rabbitmq", func() inputs.Input {
|
||||
return &RabbitMQ{}
|
||||
})
|
||||
}
|
||||
444
plugins/inputs/rabbitmq/rabbitmq_test.go
Normal file
444
plugins/inputs/rabbitmq/rabbitmq_test.go
Normal file
@@ -0,0 +1,444 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const sampleOverviewResponse = `
|
||||
{
|
||||
"message_stats": {
|
||||
"ack": 5246,
|
||||
"ack_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"deliver": 5246,
|
||||
"deliver_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"deliver_get": 5246,
|
||||
"deliver_get_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"publish": 5258,
|
||||
"publish_details": {
|
||||
"rate": 0.0
|
||||
}
|
||||
},
|
||||
"object_totals": {
|
||||
"channels": 44,
|
||||
"connections": 44,
|
||||
"consumers": 65,
|
||||
"exchanges": 43,
|
||||
"queues": 62
|
||||
},
|
||||
"queue_totals": {
|
||||
"messages": 0,
|
||||
"messages_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"messages_ready": 0,
|
||||
"messages_ready_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"messages_unacknowledged": 0,
|
||||
"messages_unacknowledged_details": {
|
||||
"rate": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const sampleNodesResponse = `
|
||||
[
|
||||
{
|
||||
"db_dir": "/var/lib/rabbitmq/mnesia/rabbit@vagrant-ubuntu-trusty-64",
|
||||
"disk_free": 37768282112,
|
||||
"disk_free_alarm": false,
|
||||
"disk_free_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"disk_free_limit": 50000000,
|
||||
"enabled_plugins": [
|
||||
"rabbitmq_management"
|
||||
],
|
||||
"fd_total": 1024,
|
||||
"fd_used": 63,
|
||||
"fd_used_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"io_read_avg_time": 0,
|
||||
"io_read_avg_time_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"io_read_bytes": 1,
|
||||
"io_read_bytes_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"io_read_count": 1,
|
||||
"io_read_count_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"io_sync_avg_time": 0,
|
||||
"io_sync_avg_time_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"io_write_avg_time": 0,
|
||||
"io_write_avg_time_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"log_file": "/var/log/rabbitmq/rabbit@vagrant-ubuntu-trusty-64.log",
|
||||
"mem_alarm": false,
|
||||
"mem_limit": 2503771750,
|
||||
"mem_used": 159707080,
|
||||
"mem_used_details": {
|
||||
"rate": 15185.6
|
||||
},
|
||||
"mnesia_disk_tx_count": 16,
|
||||
"mnesia_disk_tx_count_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"mnesia_ram_tx_count": 296,
|
||||
"mnesia_ram_tx_count_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"name": "rabbit@vagrant-ubuntu-trusty-64",
|
||||
"net_ticktime": 60,
|
||||
"os_pid": "14244",
|
||||
"partitions": [],
|
||||
"proc_total": 1048576,
|
||||
"proc_used": 783,
|
||||
"proc_used_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"processors": 1,
|
||||
"rates_mode": "basic",
|
||||
"run_queue": 0,
|
||||
"running": true,
|
||||
"sasl_log_file": "/var/log/rabbitmq/rabbit@vagrant-ubuntu-trusty-64-sasl.log",
|
||||
"sockets_total": 829,
|
||||
"sockets_used": 45,
|
||||
"sockets_used_details": {
|
||||
"rate": 0.0
|
||||
},
|
||||
"type": "disc",
|
||||
"uptime": 7464827
|
||||
}
|
||||
]
|
||||
`
|
||||
const sampleQueuesResponse = `
|
||||
[
|
||||
{
|
||||
"memory": 21960,
|
||||
"messages": 0,
|
||||
"messages_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"messages_ready": 0,
|
||||
"messages_ready_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"messages_unacknowledged": 0,
|
||||
"messages_unacknowledged_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"idle_since": "2015-11-01 8:22:15",
|
||||
"consumer_utilisation": "",
|
||||
"policy": "federator",
|
||||
"exclusive_consumer_tag": "",
|
||||
"consumers": 0,
|
||||
"recoverable_slaves": "",
|
||||
"state": "running",
|
||||
"messages_ram": 0,
|
||||
"messages_ready_ram": 0,
|
||||
"messages_unacknowledged_ram": 0,
|
||||
"messages_persistent": 0,
|
||||
"message_bytes": 0,
|
||||
"message_bytes_ready": 0,
|
||||
"message_bytes_unacknowledged": 0,
|
||||
"message_bytes_ram": 0,
|
||||
"message_bytes_persistent": 0,
|
||||
"disk_reads": 0,
|
||||
"disk_writes": 0,
|
||||
"backing_queue_status": {
|
||||
"q1": 0,
|
||||
"q2": 0,
|
||||
"delta": [
|
||||
"delta",
|
||||
"undefined",
|
||||
0,
|
||||
"undefined"
|
||||
],
|
||||
"q3": 0,
|
||||
"q4": 0,
|
||||
"len": 0,
|
||||
"target_ram_count": "infinity",
|
||||
"next_seq_id": 0,
|
||||
"avg_ingress_rate": 0,
|
||||
"avg_egress_rate": 0,
|
||||
"avg_ack_ingress_rate": 0,
|
||||
"avg_ack_egress_rate": 0
|
||||
},
|
||||
"name": "collectd-queue",
|
||||
"vhost": "collectd",
|
||||
"durable": true,
|
||||
"auto_delete": false,
|
||||
"arguments": {},
|
||||
"node": "rabbit@testhost"
|
||||
},
|
||||
{
|
||||
"memory": 55528,
|
||||
"message_stats": {
|
||||
"ack": 223654927,
|
||||
"ack_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"deliver": 224518745,
|
||||
"deliver_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"deliver_get": 224518829,
|
||||
"deliver_get_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"get": 19,
|
||||
"get_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"get_no_ack": 65,
|
||||
"get_no_ack_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"publish": 223883765,
|
||||
"publish_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"redeliver": 863805,
|
||||
"redeliver_details": {
|
||||
"rate": 0
|
||||
}
|
||||
},
|
||||
"messages": 24,
|
||||
"messages_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"messages_ready": 24,
|
||||
"messages_ready_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"messages_unacknowledged": 0,
|
||||
"messages_unacknowledged_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"idle_since": "2015-11-01 8:22:14",
|
||||
"consumer_utilisation": "",
|
||||
"policy": "",
|
||||
"exclusive_consumer_tag": "",
|
||||
"consumers": 0,
|
||||
"recoverable_slaves": "",
|
||||
"state": "running",
|
||||
"messages_ram": 24,
|
||||
"messages_ready_ram": 24,
|
||||
"messages_unacknowledged_ram": 0,
|
||||
"messages_persistent": 0,
|
||||
"message_bytes": 149220,
|
||||
"message_bytes_ready": 149220,
|
||||
"message_bytes_unacknowledged": 0,
|
||||
"message_bytes_ram": 149220,
|
||||
"message_bytes_persistent": 0,
|
||||
"disk_reads": 0,
|
||||
"disk_writes": 0,
|
||||
"backing_queue_status": {
|
||||
"q1": 0,
|
||||
"q2": 0,
|
||||
"delta": [
|
||||
"delta",
|
||||
"undefined",
|
||||
0,
|
||||
"undefined"
|
||||
],
|
||||
"q3": 0,
|
||||
"q4": 24,
|
||||
"len": 24,
|
||||
"target_ram_count": "infinity",
|
||||
"next_seq_id": 223883765,
|
||||
"avg_ingress_rate": 0,
|
||||
"avg_egress_rate": 0,
|
||||
"avg_ack_ingress_rate": 0,
|
||||
"avg_ack_egress_rate": 0
|
||||
},
|
||||
"name": "telegraf",
|
||||
"vhost": "collectd",
|
||||
"durable": true,
|
||||
"auto_delete": false,
|
||||
"arguments": {},
|
||||
"node": "rabbit@testhost"
|
||||
},
|
||||
{
|
||||
"message_stats": {
|
||||
"ack": 1296077,
|
||||
"ack_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"deliver": 1513176,
|
||||
"deliver_details": {
|
||||
"rate": 0.4
|
||||
},
|
||||
"deliver_get": 1513239,
|
||||
"deliver_get_details": {
|
||||
"rate": 0.4
|
||||
},
|
||||
"disk_writes": 7976,
|
||||
"disk_writes_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"get": 40,
|
||||
"get_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"get_no_ack": 23,
|
||||
"get_no_ack_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"publish": 1325628,
|
||||
"publish_details": {
|
||||
"rate": 0.4
|
||||
},
|
||||
"redeliver": 216034,
|
||||
"redeliver_details": {
|
||||
"rate": 0
|
||||
}
|
||||
},
|
||||
"messages": 5,
|
||||
"messages_details": {
|
||||
"rate": 0.4
|
||||
},
|
||||
"messages_ready": 0,
|
||||
"messages_ready_details": {
|
||||
"rate": 0
|
||||
},
|
||||
"messages_unacknowledged": 5,
|
||||
"messages_unacknowledged_details": {
|
||||
"rate": 0.4
|
||||
},
|
||||
"policy": "federator",
|
||||
"exclusive_consumer_tag": "",
|
||||
"consumers": 1,
|
||||
"consumer_utilisation": 1,
|
||||
"memory": 122856,
|
||||
"recoverable_slaves": "",
|
||||
"state": "running",
|
||||
"messages_ram": 5,
|
||||
"messages_ready_ram": 0,
|
||||
"messages_unacknowledged_ram": 5,
|
||||
"messages_persistent": 0,
|
||||
"message_bytes": 150096,
|
||||
"message_bytes_ready": 0,
|
||||
"message_bytes_unacknowledged": 150096,
|
||||
"message_bytes_ram": 150096,
|
||||
"message_bytes_persistent": 0,
|
||||
"disk_reads": 0,
|
||||
"disk_writes": 7976,
|
||||
"backing_queue_status": {
|
||||
"q1": 0,
|
||||
"q2": 0,
|
||||
"delta": [
|
||||
"delta",
|
||||
"undefined",
|
||||
0,
|
||||
"undefined"
|
||||
],
|
||||
"q3": 0,
|
||||
"q4": 0,
|
||||
"len": 0,
|
||||
"target_ram_count": "infinity",
|
||||
"next_seq_id": 1325628,
|
||||
"avg_ingress_rate": 0.19115840579934168,
|
||||
"avg_egress_rate": 0.19115840579934168,
|
||||
"avg_ack_ingress_rate": 0.19115840579934168,
|
||||
"avg_ack_egress_rate": 0.1492766485341716
|
||||
},
|
||||
"name": "telegraf",
|
||||
"vhost": "metrics",
|
||||
"durable": true,
|
||||
"auto_delete": false,
|
||||
"arguments": {},
|
||||
"node": "rabbit@testhost"
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
func TestRabbitMQGeneratesMetrics(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var rsp string
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/api/overview":
|
||||
rsp = sampleOverviewResponse
|
||||
case "/api/nodes":
|
||||
rsp = sampleNodesResponse
|
||||
case "/api/queues":
|
||||
rsp = sampleQueuesResponse
|
||||
default:
|
||||
panic("Cannot handle request")
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, rsp)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
r := &RabbitMQ{
|
||||
URL: ts.URL,
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := r.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
intMetrics := []string{
|
||||
"messages",
|
||||
"messages_ready",
|
||||
"messages_unacked",
|
||||
|
||||
"messages_acked",
|
||||
"messages_delivered",
|
||||
"messages_published",
|
||||
|
||||
"channels",
|
||||
"connections",
|
||||
"consumers",
|
||||
"exchanges",
|
||||
"queues",
|
||||
}
|
||||
|
||||
for _, metric := range intMetrics {
|
||||
assert.True(t, acc.HasIntField("rabbitmq_overview", metric))
|
||||
}
|
||||
|
||||
nodeIntMetrics := []string{
|
||||
"disk_free",
|
||||
"disk_free_limit",
|
||||
"fd_total",
|
||||
"fd_used",
|
||||
"mem_limit",
|
||||
"mem_used",
|
||||
"proc_total",
|
||||
"proc_used",
|
||||
"run_queue",
|
||||
"sockets_total",
|
||||
"sockets_used",
|
||||
}
|
||||
|
||||
for _, metric := range nodeIntMetrics {
|
||||
assert.True(t, acc.HasIntField("rabbitmq_node", metric))
|
||||
}
|
||||
|
||||
assert.True(t, acc.HasMeasurement("rabbitmq_queue"))
|
||||
}
|
||||
252
plugins/inputs/redis/redis.go
Normal file
252
plugins/inputs/redis/redis.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type Redis struct {
|
||||
Servers []string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# specify servers via a url matching:
|
||||
# [protocol://][:password]@address[:port]
|
||||
# e.g.
|
||||
# tcp://localhost:6379
|
||||
# tcp://:password@192.168.99.100
|
||||
#
|
||||
# If no servers are specified, then localhost is used as the host.
|
||||
# If no port is specified, 6379 is used
|
||||
servers = ["tcp://localhost:6379"]
|
||||
`
|
||||
|
||||
func (r *Redis) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (r *Redis) Description() string {
|
||||
return "Read metrics from one or many redis servers"
|
||||
}
|
||||
|
||||
var Tracking = map[string]string{
|
||||
"uptime_in_seconds": "uptime",
|
||||
"connected_clients": "clients",
|
||||
"used_memory": "used_memory",
|
||||
"used_memory_rss": "used_memory_rss",
|
||||
"used_memory_peak": "used_memory_peak",
|
||||
"used_memory_lua": "used_memory_lua",
|
||||
"rdb_changes_since_last_save": "rdb_changes_since_last_save",
|
||||
"total_connections_received": "total_connections_received",
|
||||
"total_commands_processed": "total_commands_processed",
|
||||
"instantaneous_ops_per_sec": "instantaneous_ops_per_sec",
|
||||
"instantaneous_input_kbps": "instantaneous_input_kbps",
|
||||
"instantaneous_output_kbps": "instantaneous_output_kbps",
|
||||
"sync_full": "sync_full",
|
||||
"sync_partial_ok": "sync_partial_ok",
|
||||
"sync_partial_err": "sync_partial_err",
|
||||
"expired_keys": "expired_keys",
|
||||
"evicted_keys": "evicted_keys",
|
||||
"keyspace_hits": "keyspace_hits",
|
||||
"keyspace_misses": "keyspace_misses",
|
||||
"pubsub_channels": "pubsub_channels",
|
||||
"pubsub_patterns": "pubsub_patterns",
|
||||
"latest_fork_usec": "latest_fork_usec",
|
||||
"connected_slaves": "connected_slaves",
|
||||
"master_repl_offset": "master_repl_offset",
|
||||
"repl_backlog_active": "repl_backlog_active",
|
||||
"repl_backlog_size": "repl_backlog_size",
|
||||
"repl_backlog_histlen": "repl_backlog_histlen",
|
||||
"mem_fragmentation_ratio": "mem_fragmentation_ratio",
|
||||
"used_cpu_sys": "used_cpu_sys",
|
||||
"used_cpu_user": "used_cpu_user",
|
||||
"used_cpu_sys_children": "used_cpu_sys_children",
|
||||
"used_cpu_user_children": "used_cpu_user_children",
|
||||
}
|
||||
|
||||
var ErrProtocolError = errors.New("redis protocol error")
|
||||
|
||||
// Reads stats from all configured servers accumulates stats.
|
||||
// Returns one of the errors encountered while gather stats (if any).
|
||||
func (r *Redis) Gather(acc inputs.Accumulator) error {
|
||||
if len(r.Servers) == 0 {
|
||||
url := &url.URL{
|
||||
Host: ":6379",
|
||||
}
|
||||
r.gatherServer(url, acc)
|
||||
return nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var outerr error
|
||||
|
||||
for _, serv := range r.Servers {
|
||||
u, err := url.Parse(serv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse to address '%s': %s", serv, err)
|
||||
} else if u.Scheme == "" {
|
||||
// fallback to simple string based address (i.e. "10.0.0.1:10000")
|
||||
u.Scheme = "tcp"
|
||||
u.Host = serv
|
||||
u.Path = ""
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(serv string) {
|
||||
defer wg.Done()
|
||||
outerr = r.gatherServer(u, acc)
|
||||
}(serv)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
const defaultPort = "6379"
|
||||
|
||||
func (r *Redis) gatherServer(addr *url.URL, acc inputs.Accumulator) error {
|
||||
_, _, err := net.SplitHostPort(addr.Host)
|
||||
if err != nil {
|
||||
addr.Host = addr.Host + ":" + defaultPort
|
||||
}
|
||||
|
||||
c, err := net.Dial("tcp", addr.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to connect to redis server '%s': %s", addr.Host, err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if addr.User != nil {
|
||||
pwd, set := addr.User.Password()
|
||||
if set && pwd != "" {
|
||||
c.Write([]byte(fmt.Sprintf("AUTH %s\r\n", pwd)))
|
||||
|
||||
rdr := bufio.NewReader(c)
|
||||
|
||||
line, err := rdr.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if line[0] != '+' {
|
||||
return fmt.Errorf("%s", strings.TrimSpace(line)[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Write([]byte("INFO\r\n"))
|
||||
c.Write([]byte("EOF\r\n"))
|
||||
rdr := bufio.NewReader(c)
|
||||
|
||||
// Setup tags for all redis metrics
|
||||
host, port := "unknown", "unknown"
|
||||
// If there's an error, ignore and use 'unknown' tags
|
||||
host, port, _ = net.SplitHostPort(addr.Host)
|
||||
tags := map[string]string{"server": host, "port": port}
|
||||
|
||||
return gatherInfoOutput(rdr, acc, tags)
|
||||
}
|
||||
|
||||
// gatherInfoOutput gathers
|
||||
func gatherInfoOutput(
|
||||
rdr *bufio.Reader,
|
||||
acc inputs.Accumulator,
|
||||
tags map[string]string,
|
||||
) error {
|
||||
var keyspace_hits, keyspace_misses uint64 = 0, 0
|
||||
|
||||
scanner := bufio.NewScanner(rdr)
|
||||
fields := make(map[string]interface{})
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(line, "ERR") {
|
||||
break
|
||||
}
|
||||
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
name := string(parts[0])
|
||||
metric, ok := Tracking[name]
|
||||
if !ok {
|
||||
kline := strings.TrimSpace(string(parts[1]))
|
||||
gatherKeyspaceLine(name, kline, acc, tags)
|
||||
continue
|
||||
}
|
||||
|
||||
val := strings.TrimSpace(parts[1])
|
||||
ival, err := strconv.ParseUint(val, 10, 64)
|
||||
|
||||
if name == "keyspace_hits" {
|
||||
keyspace_hits = ival
|
||||
}
|
||||
|
||||
if name == "keyspace_misses" {
|
||||
keyspace_misses = ival
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
fields[metric] = ival
|
||||
continue
|
||||
}
|
||||
|
||||
fval, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields[metric] = fval
|
||||
}
|
||||
var keyspace_hitrate float64 = 0.0
|
||||
if keyspace_hits != 0 || keyspace_misses != 0 {
|
||||
keyspace_hitrate = float64(keyspace_hits) / float64(keyspace_hits+keyspace_misses)
|
||||
}
|
||||
fields["keyspace_hitrate"] = keyspace_hitrate
|
||||
acc.AddFields("redis", fields, tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the special Keyspace line at end of redis stats
|
||||
// This is a special line that looks something like:
|
||||
// db0:keys=2,expires=0,avg_ttl=0
|
||||
// And there is one for each db on the redis instance
|
||||
func gatherKeyspaceLine(
|
||||
name string,
|
||||
line string,
|
||||
acc inputs.Accumulator,
|
||||
tags map[string]string,
|
||||
) {
|
||||
if strings.Contains(line, "keys=") {
|
||||
fields := make(map[string]interface{})
|
||||
tags["database"] = name
|
||||
dbparts := strings.Split(line, ",")
|
||||
for _, dbp := range dbparts {
|
||||
kv := strings.Split(dbp, "=")
|
||||
ival, err := strconv.ParseUint(kv[1], 10, 64)
|
||||
if err == nil {
|
||||
fields[kv[0]] = ival
|
||||
}
|
||||
}
|
||||
acc.AddFields("redis_keyspace", fields, tags)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("redis", func() inputs.Input {
|
||||
return &Redis{}
|
||||
})
|
||||
}
|
||||
170
plugins/inputs/redis/redis_test.go
Normal file
170
plugins/inputs/redis/redis_test.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRedisConnect(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf(testutil.GetLocalHost() + ":6379")
|
||||
|
||||
r := &Redis{
|
||||
Servers: []string{addr},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := r.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRedis_ParseMetrics(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
tags := map[string]string{"host": "redis.net"}
|
||||
rdr := bufio.NewReader(strings.NewReader(testOutput))
|
||||
|
||||
err := gatherInfoOutput(rdr, &acc, tags)
|
||||
require.NoError(t, err)
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"uptime": uint64(238),
|
||||
"clients": uint64(1),
|
||||
"used_memory": uint64(1003936),
|
||||
"used_memory_rss": uint64(811008),
|
||||
"used_memory_peak": uint64(1003936),
|
||||
"used_memory_lua": uint64(33792),
|
||||
"rdb_changes_since_last_save": uint64(0),
|
||||
"total_connections_received": uint64(2),
|
||||
"total_commands_processed": uint64(1),
|
||||
"instantaneous_ops_per_sec": uint64(0),
|
||||
"sync_full": uint64(0),
|
||||
"sync_partial_ok": uint64(0),
|
||||
"sync_partial_err": uint64(0),
|
||||
"expired_keys": uint64(0),
|
||||
"evicted_keys": uint64(0),
|
||||
"keyspace_hits": uint64(1),
|
||||
"keyspace_misses": uint64(1),
|
||||
"pubsub_channels": uint64(0),
|
||||
"pubsub_patterns": uint64(0),
|
||||
"latest_fork_usec": uint64(0),
|
||||
"connected_slaves": uint64(0),
|
||||
"master_repl_offset": uint64(0),
|
||||
"repl_backlog_active": uint64(0),
|
||||
"repl_backlog_size": uint64(1048576),
|
||||
"repl_backlog_histlen": uint64(0),
|
||||
"mem_fragmentation_ratio": float64(0.81),
|
||||
"instantaneous_input_kbps": float64(876.16),
|
||||
"instantaneous_output_kbps": float64(3010.23),
|
||||
"used_cpu_sys": float64(0.14),
|
||||
"used_cpu_user": float64(0.05),
|
||||
"used_cpu_sys_children": float64(0.00),
|
||||
"used_cpu_user_children": float64(0.00),
|
||||
"keyspace_hitrate": float64(0.50),
|
||||
}
|
||||
keyspaceFields := map[string]interface{}{
|
||||
"avg_ttl": uint64(0),
|
||||
"expires": uint64(0),
|
||||
"keys": uint64(2),
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "redis", fields, tags)
|
||||
acc.AssertContainsTaggedFields(t, "redis_keyspace", keyspaceFields, tags)
|
||||
}
|
||||
|
||||
const testOutput = `# Server
|
||||
redis_version:2.8.9
|
||||
redis_git_sha1:00000000
|
||||
redis_git_dirty:0
|
||||
redis_build_id:9ccc8119ea98f6e1
|
||||
redis_mode:standalone
|
||||
os:Darwin 14.1.0 x86_64
|
||||
arch_bits:64
|
||||
multiplexing_api:kqueue
|
||||
gcc_version:4.2.1
|
||||
process_id:40235
|
||||
run_id:37d020620aadf0627282c0f3401405d774a82664
|
||||
tcp_port:6379
|
||||
uptime_in_seconds:238
|
||||
uptime_in_days:0
|
||||
hz:10
|
||||
lru_clock:2364819
|
||||
config_file:/usr/local/etc/redis.conf
|
||||
|
||||
# Clients
|
||||
connected_clients:1
|
||||
client_longest_output_list:0
|
||||
client_biggest_input_buf:0
|
||||
blocked_clients:0
|
||||
|
||||
# Memory
|
||||
used_memory:1003936
|
||||
used_memory_human:980.41K
|
||||
used_memory_rss:811008
|
||||
used_memory_peak:1003936
|
||||
used_memory_peak_human:980.41K
|
||||
used_memory_lua:33792
|
||||
mem_fragmentation_ratio:0.81
|
||||
mem_allocator:libc
|
||||
|
||||
# Persistence
|
||||
loading:0
|
||||
rdb_changes_since_last_save:0
|
||||
rdb_bgsave_in_progress:0
|
||||
rdb_last_save_time:1428427941
|
||||
rdb_last_bgsave_status:ok
|
||||
rdb_last_bgsave_time_sec:-1
|
||||
rdb_current_bgsave_time_sec:-1
|
||||
aof_enabled:0
|
||||
aof_rewrite_in_progress:0
|
||||
aof_rewrite_scheduled:0
|
||||
aof_last_rewrite_time_sec:-1
|
||||
aof_current_rewrite_time_sec:-1
|
||||
aof_last_bgrewrite_status:ok
|
||||
aof_last_write_status:ok
|
||||
|
||||
# Stats
|
||||
total_connections_received:2
|
||||
total_commands_processed:1
|
||||
instantaneous_ops_per_sec:0
|
||||
instantaneous_input_kbps:876.16
|
||||
instantaneous_output_kbps:3010.23
|
||||
rejected_connections:0
|
||||
sync_full:0
|
||||
sync_partial_ok:0
|
||||
sync_partial_err:0
|
||||
expired_keys:0
|
||||
evicted_keys:0
|
||||
keyspace_hits:1
|
||||
keyspace_misses:1
|
||||
pubsub_channels:0
|
||||
pubsub_patterns:0
|
||||
latest_fork_usec:0
|
||||
|
||||
# Replication
|
||||
role:master
|
||||
connected_slaves:0
|
||||
master_repl_offset:0
|
||||
repl_backlog_active:0
|
||||
repl_backlog_size:1048576
|
||||
repl_backlog_first_byte_offset:0
|
||||
repl_backlog_histlen:0
|
||||
|
||||
# CPU
|
||||
used_cpu_sys:0.14
|
||||
used_cpu_user:0.05
|
||||
used_cpu_sys_children:0.00
|
||||
used_cpu_user_children:0.00
|
||||
|
||||
# Keyspace
|
||||
db0:keys=2,expires=0,avg_ttl=0
|
||||
|
||||
(error) ERR unknown command 'eof'
|
||||
`
|
||||
56
plugins/inputs/registry.go
Normal file
56
plugins/inputs/registry.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package inputs
|
||||
|
||||
import "time"
|
||||
|
||||
type Accumulator interface {
|
||||
// Create a point with a value, decorating it with tags
|
||||
// NOTE: tags is expected to be owned by the caller, don't mutate
|
||||
// it after passing to Add.
|
||||
Add(measurement string,
|
||||
value interface{},
|
||||
tags map[string]string,
|
||||
t ...time.Time)
|
||||
|
||||
AddFields(measurement string,
|
||||
fields map[string]interface{},
|
||||
tags map[string]string,
|
||||
t ...time.Time)
|
||||
}
|
||||
|
||||
type Input interface {
|
||||
// SampleConfig returns the default configuration of the Input
|
||||
SampleConfig() string
|
||||
|
||||
// Description returns a one-sentence description on the Input
|
||||
Description() string
|
||||
|
||||
// Gather takes in an accumulator and adds the metrics that the Input
|
||||
// gathers. This is called every "interval"
|
||||
Gather(Accumulator) error
|
||||
}
|
||||
|
||||
type ServiceInput interface {
|
||||
// SampleConfig returns the default configuration of the Input
|
||||
SampleConfig() string
|
||||
|
||||
// Description returns a one-sentence description on the Input
|
||||
Description() string
|
||||
|
||||
// Gather takes in an accumulator and adds the metrics that the Input
|
||||
// gathers. This is called every "interval"
|
||||
Gather(Accumulator) error
|
||||
|
||||
// Start starts the ServiceInput's service, whatever that may be
|
||||
Start() error
|
||||
|
||||
// Stop stops the services and closes any necessary channels and connections
|
||||
Stop()
|
||||
}
|
||||
|
||||
type Creator func() Input
|
||||
|
||||
var Inputs = map[string]Creator{}
|
||||
|
||||
func Add(name string, creator Creator) {
|
||||
Inputs[name] = creator
|
||||
}
|
||||
93
plugins/inputs/rethinkdb/rethinkdb.go
Normal file
93
plugins/inputs/rethinkdb/rethinkdb.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package rethinkdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
|
||||
"gopkg.in/dancannon/gorethink.v1"
|
||||
)
|
||||
|
||||
type RethinkDB struct {
|
||||
Servers []string
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# An array of URI to gather stats about. Specify an ip or hostname
|
||||
# with optional port add password. ie rethinkdb://user:auth_key@10.10.3.30:28105,
|
||||
# rethinkdb://10.10.3.33:18832, 10.0.0.1:10000, etc.
|
||||
#
|
||||
# If no servers are specified, then 127.0.0.1 is used as the host and 28015 as the port.
|
||||
servers = ["127.0.0.1:28015"]
|
||||
`
|
||||
|
||||
func (r *RethinkDB) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (r *RethinkDB) Description() string {
|
||||
return "Read metrics from one or many RethinkDB servers"
|
||||
}
|
||||
|
||||
var localhost = &Server{Url: &url.URL{Host: "127.0.0.1:28015"}}
|
||||
|
||||
// Reads stats from all configured servers accumulates stats.
|
||||
// Returns one of the errors encountered while gather stats (if any).
|
||||
func (r *RethinkDB) Gather(acc inputs.Accumulator) error {
|
||||
if len(r.Servers) == 0 {
|
||||
r.gatherServer(localhost, acc)
|
||||
return nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var outerr error
|
||||
|
||||
for _, serv := range r.Servers {
|
||||
u, err := url.Parse(serv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to parse to address '%s': %s", serv, err)
|
||||
} else if u.Scheme == "" {
|
||||
// fallback to simple string based address (i.e. "10.0.0.1:10000")
|
||||
u.Host = serv
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(serv string) {
|
||||
defer wg.Done()
|
||||
outerr = r.gatherServer(&Server{Url: u}, acc)
|
||||
}(serv)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
func (r *RethinkDB) gatherServer(server *Server, acc inputs.Accumulator) error {
|
||||
var err error
|
||||
connectOpts := gorethink.ConnectOpts{
|
||||
Address: server.Url.Host,
|
||||
DiscoverHosts: false,
|
||||
}
|
||||
if server.Url.User != nil {
|
||||
pwd, set := server.Url.User.Password()
|
||||
if set && pwd != "" {
|
||||
connectOpts.AuthKey = pwd
|
||||
}
|
||||
}
|
||||
server.session, err = gorethink.Connect(connectOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to connect to RethinkDB, %s\n", err.Error())
|
||||
}
|
||||
defer server.session.Close()
|
||||
|
||||
return server.gatherData(acc)
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("rethinkdb", func() inputs.Input {
|
||||
return &RethinkDB{}
|
||||
})
|
||||
}
|
||||
115
plugins/inputs/rethinkdb/rethinkdb_data.go
Normal file
115
plugins/inputs/rethinkdb/rethinkdb_data.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package rethinkdb
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type serverStatus struct {
|
||||
Id string `gorethink:"id"`
|
||||
Network struct {
|
||||
Addresses []Address `gorethink:"canonical_addresses"`
|
||||
Hostname string `gorethink:"hostname"`
|
||||
DriverPort int `gorethink:"reql_port"`
|
||||
} `gorethink:"network"`
|
||||
Process struct {
|
||||
Version string `gorethink:"version"`
|
||||
RunningSince time.Time `gorethink:"time_started"`
|
||||
} `gorethink:"process"`
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
Host string `gorethink:"host"`
|
||||
Port int `gorethink:"port"`
|
||||
}
|
||||
|
||||
type stats struct {
|
||||
Engine Engine `gorethink:"query_engine"`
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
ClientConns int64 `gorethink:"client_connections,omitempty"`
|
||||
ClientActive int64 `gorethink:"clients_active,omitempty"`
|
||||
QueriesPerSec int64 `gorethink:"queries_per_sec,omitempty"`
|
||||
TotalQueries int64 `gorethink:"queries_total,omitempty"`
|
||||
ReadsPerSec int64 `gorethink:"read_docs_per_sec,omitempty"`
|
||||
TotalReads int64 `gorethink:"read_docs_total,omitempty"`
|
||||
WritesPerSec int64 `gorethink:"written_docs_per_sec,omitempty"`
|
||||
TotalWrites int64 `gorethink:"written_docs_total,omitempty"`
|
||||
}
|
||||
|
||||
type tableStatus struct {
|
||||
Id string `gorethink:"id"`
|
||||
DB string `gorethink:"db"`
|
||||
Name string `gorethink:"name"`
|
||||
}
|
||||
|
||||
type tableStats struct {
|
||||
Engine Engine `gorethink:"query_engine"`
|
||||
Storage Storage `gorethink:"storage_engine"`
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
Cache Cache `gorethink:"cache"`
|
||||
Disk Disk `gorethink:"disk"`
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
BytesInUse int64 `gorethink:"in_use_bytes"`
|
||||
}
|
||||
|
||||
type Disk struct {
|
||||
ReadBytesPerSec int64 `gorethink:"read_bytes_per_sec"`
|
||||
ReadBytesTotal int64 `gorethink:"read_bytes_total"`
|
||||
WriteBytesPerSec int64 `gorethik:"written_bytes_per_sec"`
|
||||
WriteBytesTotal int64 `gorethink:"written_bytes_total"`
|
||||
SpaceUsage SpaceUsage `gorethink:"space_usage"`
|
||||
}
|
||||
|
||||
type SpaceUsage struct {
|
||||
Data int64 `gorethink:"data_bytes"`
|
||||
Garbage int64 `gorethink:"garbage_bytes"`
|
||||
Metadata int64 `gorethink:"metadata_bytes"`
|
||||
Prealloc int64 `gorethink:"preallocated_bytes"`
|
||||
}
|
||||
|
||||
var engineStats = map[string]string{
|
||||
"active_clients": "ClientActive",
|
||||
"clients": "ClientConns",
|
||||
"queries_per_sec": "QueriesPerSec",
|
||||
"total_queries": "TotalQueries",
|
||||
"read_docs_per_sec": "ReadsPerSec",
|
||||
"total_reads": "TotalReads",
|
||||
"written_docs_per_sec": "WritesPerSec",
|
||||
"total_writes": "TotalWrites",
|
||||
}
|
||||
|
||||
func (e *Engine) AddEngineStats(
|
||||
keys []string,
|
||||
acc inputs.Accumulator,
|
||||
tags map[string]string,
|
||||
) {
|
||||
engine := reflect.ValueOf(e).Elem()
|
||||
fields := make(map[string]interface{})
|
||||
for _, key := range keys {
|
||||
fields[key] = engine.FieldByName(engineStats[key]).Interface()
|
||||
}
|
||||
acc.AddFields("rethinkdb_engine", fields, tags)
|
||||
}
|
||||
|
||||
func (s *Storage) AddStats(acc inputs.Accumulator, tags map[string]string) {
|
||||
fields := map[string]interface{}{
|
||||
"cache_bytes_in_use": s.Cache.BytesInUse,
|
||||
"disk_read_bytes_per_sec": s.Disk.ReadBytesPerSec,
|
||||
"disk_read_bytes_total": s.Disk.ReadBytesTotal,
|
||||
"disk_written_bytes_per_sec": s.Disk.WriteBytesPerSec,
|
||||
"disk_written_bytes_total": s.Disk.WriteBytesTotal,
|
||||
"disk_usage_data_bytes": s.Disk.SpaceUsage.Data,
|
||||
"disk_usage_garbage_bytes": s.Disk.SpaceUsage.Garbage,
|
||||
"disk_usage_metadata_bytes": s.Disk.SpaceUsage.Metadata,
|
||||
"disk_usage_preallocated_bytes": s.Disk.SpaceUsage.Prealloc,
|
||||
}
|
||||
acc.AddFields("rethinkdb", fields, tags)
|
||||
}
|
||||
112
plugins/inputs/rethinkdb/rethinkdb_data_test.go
Normal file
112
plugins/inputs/rethinkdb/rethinkdb_data_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package rethinkdb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var tags = make(map[string]string)
|
||||
|
||||
func TestAddEngineStats(t *testing.T) {
|
||||
engine := &Engine{
|
||||
ClientConns: 0,
|
||||
ClientActive: 0,
|
||||
QueriesPerSec: 0,
|
||||
TotalQueries: 0,
|
||||
ReadsPerSec: 0,
|
||||
TotalReads: 0,
|
||||
WritesPerSec: 0,
|
||||
TotalWrites: 0,
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
keys := []string{
|
||||
"active_clients",
|
||||
"clients",
|
||||
"queries_per_sec",
|
||||
"total_queries",
|
||||
"read_docs_per_sec",
|
||||
"total_reads",
|
||||
"written_docs_per_sec",
|
||||
"total_writes",
|
||||
}
|
||||
engine.AddEngineStats(keys, &acc, tags)
|
||||
|
||||
for _, metric := range keys {
|
||||
assert.True(t, acc.HasIntField("rethinkdb_engine", metric))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddEngineStatsPartial(t *testing.T) {
|
||||
engine := &Engine{
|
||||
ClientConns: 0,
|
||||
ClientActive: 0,
|
||||
QueriesPerSec: 0,
|
||||
ReadsPerSec: 0,
|
||||
WritesPerSec: 0,
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
keys := []string{
|
||||
"active_clients",
|
||||
"clients",
|
||||
"queries_per_sec",
|
||||
"read_docs_per_sec",
|
||||
"written_docs_per_sec",
|
||||
}
|
||||
|
||||
missing_keys := []string{
|
||||
"total_queries",
|
||||
"total_reads",
|
||||
"total_writes",
|
||||
}
|
||||
engine.AddEngineStats(keys, &acc, tags)
|
||||
|
||||
for _, metric := range missing_keys {
|
||||
assert.False(t, acc.HasIntField("rethinkdb", metric))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddStorageStats(t *testing.T) {
|
||||
storage := &Storage{
|
||||
Cache: Cache{
|
||||
BytesInUse: 0,
|
||||
},
|
||||
Disk: Disk{
|
||||
ReadBytesPerSec: 0,
|
||||
ReadBytesTotal: 0,
|
||||
WriteBytesPerSec: 0,
|
||||
WriteBytesTotal: 0,
|
||||
SpaceUsage: SpaceUsage{
|
||||
Data: 0,
|
||||
Garbage: 0,
|
||||
Metadata: 0,
|
||||
Prealloc: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
keys := []string{
|
||||
"cache_bytes_in_use",
|
||||
"disk_read_bytes_per_sec",
|
||||
"disk_read_bytes_total",
|
||||
"disk_written_bytes_per_sec",
|
||||
"disk_written_bytes_total",
|
||||
"disk_usage_data_bytes",
|
||||
"disk_usage_garbage_bytes",
|
||||
"disk_usage_metadata_bytes",
|
||||
"disk_usage_preallocated_bytes",
|
||||
}
|
||||
|
||||
storage.AddStats(&acc, tags)
|
||||
|
||||
for _, metric := range keys {
|
||||
assert.True(t, acc.HasIntField("rethinkdb", metric))
|
||||
}
|
||||
}
|
||||
193
plugins/inputs/rethinkdb/rethinkdb_server.go
Normal file
193
plugins/inputs/rethinkdb/rethinkdb_server.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package rethinkdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
|
||||
"gopkg.in/dancannon/gorethink.v1"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Url *url.URL
|
||||
session *gorethink.Session
|
||||
serverStatus serverStatus
|
||||
}
|
||||
|
||||
func (s *Server) gatherData(acc inputs.Accumulator) error {
|
||||
if err := s.getServerStatus(); err != nil {
|
||||
return fmt.Errorf("Failed to get server_status, %s\n", err)
|
||||
}
|
||||
|
||||
if err := s.validateVersion(); err != nil {
|
||||
return fmt.Errorf("Failed version validation, %s\n", err.Error())
|
||||
}
|
||||
|
||||
if err := s.addClusterStats(acc); err != nil {
|
||||
fmt.Printf("error adding cluster stats, %s\n", err.Error())
|
||||
return fmt.Errorf("Error adding cluster stats, %s\n", err.Error())
|
||||
}
|
||||
|
||||
if err := s.addMemberStats(acc); err != nil {
|
||||
return fmt.Errorf("Error adding member stats, %s\n", err.Error())
|
||||
}
|
||||
|
||||
if err := s.addTableStats(acc); err != nil {
|
||||
return fmt.Errorf("Error adding table stats, %s\n", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) validateVersion() error {
|
||||
if s.serverStatus.Process.Version == "" {
|
||||
return errors.New("could not determine the RethinkDB server version: process.version key missing")
|
||||
}
|
||||
|
||||
versionRegexp := regexp.MustCompile("\\d.\\d.\\d")
|
||||
versionString := versionRegexp.FindString(s.serverStatus.Process.Version)
|
||||
if versionString == "" {
|
||||
return fmt.Errorf("could not determine the RethinkDB server version: malformed version string (%v)", s.serverStatus.Process.Version)
|
||||
}
|
||||
|
||||
majorVersion, err := strconv.Atoi(strings.Split(versionString, "")[0])
|
||||
if err != nil || majorVersion < 2 {
|
||||
return fmt.Errorf("unsupported major version %s\n", versionString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) getServerStatus() error {
|
||||
cursor, err := gorethink.DB("rethinkdb").Table("server_status").Run(s.session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cursor.IsNil() {
|
||||
return errors.New("could not determine the RethinkDB server version: no rows returned from the server_status table")
|
||||
}
|
||||
defer cursor.Close()
|
||||
var serverStatuses []serverStatus
|
||||
err = cursor.All(&serverStatuses)
|
||||
if err != nil {
|
||||
return errors.New("could not parse server_status results")
|
||||
}
|
||||
host, port, err := net.SplitHostPort(s.Url.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to determine provided hostname from %s\n", s.Url.Host)
|
||||
}
|
||||
driverPort, _ := strconv.Atoi(port)
|
||||
for _, ss := range serverStatuses {
|
||||
for _, address := range ss.Network.Addresses {
|
||||
if address.Host == host && ss.Network.DriverPort == driverPort {
|
||||
s.serverStatus = ss
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to determine host id from server_status with %s", s.Url.Host)
|
||||
}
|
||||
|
||||
func (s *Server) getDefaultTags() map[string]string {
|
||||
tags := make(map[string]string)
|
||||
tags["host"] = s.Url.Host
|
||||
tags["hostname"] = s.serverStatus.Network.Hostname
|
||||
return tags
|
||||
}
|
||||
|
||||
var ClusterTracking = []string{
|
||||
"active_clients",
|
||||
"clients",
|
||||
"queries_per_sec",
|
||||
"read_docs_per_sec",
|
||||
"written_docs_per_sec",
|
||||
}
|
||||
|
||||
func (s *Server) addClusterStats(acc inputs.Accumulator) error {
|
||||
cursor, err := gorethink.DB("rethinkdb").Table("stats").Get([]string{"cluster"}).Run(s.session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cluster stats query error, %s\n", err.Error())
|
||||
}
|
||||
defer cursor.Close()
|
||||
var clusterStats stats
|
||||
if err := cursor.One(&clusterStats); err != nil {
|
||||
return fmt.Errorf("failure to parse cluster stats, %s\n", err.Error())
|
||||
}
|
||||
|
||||
tags := s.getDefaultTags()
|
||||
tags["type"] = "cluster"
|
||||
clusterStats.Engine.AddEngineStats(ClusterTracking, acc, tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
var MemberTracking = []string{
|
||||
"active_clients",
|
||||
"clients",
|
||||
"queries_per_sec",
|
||||
"total_queries",
|
||||
"read_docs_per_sec",
|
||||
"total_reads",
|
||||
"written_docs_per_sec",
|
||||
"total_writes",
|
||||
}
|
||||
|
||||
func (s *Server) addMemberStats(acc inputs.Accumulator) error {
|
||||
cursor, err := gorethink.DB("rethinkdb").Table("stats").Get([]string{"server", s.serverStatus.Id}).Run(s.session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("member stats query error, %s\n", err.Error())
|
||||
}
|
||||
defer cursor.Close()
|
||||
var memberStats stats
|
||||
if err := cursor.One(&memberStats); err != nil {
|
||||
return fmt.Errorf("failure to parse member stats, %s\n", err.Error())
|
||||
}
|
||||
|
||||
tags := s.getDefaultTags()
|
||||
tags["type"] = "member"
|
||||
memberStats.Engine.AddEngineStats(MemberTracking, acc, tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
var TableTracking = []string{
|
||||
"read_docs_per_sec",
|
||||
"total_reads",
|
||||
"written_docs_per_sec",
|
||||
"total_writes",
|
||||
}
|
||||
|
||||
func (s *Server) addTableStats(acc inputs.Accumulator) error {
|
||||
tablesCursor, err := gorethink.DB("rethinkdb").Table("table_status").Run(s.session)
|
||||
defer tablesCursor.Close()
|
||||
var tables []tableStatus
|
||||
err = tablesCursor.All(&tables)
|
||||
if err != nil {
|
||||
return errors.New("could not parse table_status results")
|
||||
}
|
||||
for _, table := range tables {
|
||||
cursor, err := gorethink.DB("rethinkdb").Table("stats").
|
||||
Get([]string{"table_server", table.Id, s.serverStatus.Id}).
|
||||
Run(s.session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("table stats query error, %s\n", err.Error())
|
||||
}
|
||||
defer cursor.Close()
|
||||
var ts tableStats
|
||||
if err := cursor.One(&ts); err != nil {
|
||||
return fmt.Errorf("failure to parse table stats, %s\n", err.Error())
|
||||
}
|
||||
|
||||
tags := s.getDefaultTags()
|
||||
tags["type"] = "data"
|
||||
tags["ns"] = fmt.Sprintf("%s.%s", table.DB, table.Name)
|
||||
ts.Engine.AddEngineStats(TableTracking, acc, tags)
|
||||
ts.Storage.AddStats(acc, tags)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
81
plugins/inputs/rethinkdb/rethinkdb_server_test.go
Normal file
81
plugins/inputs/rethinkdb/rethinkdb_server_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// +build integration
|
||||
|
||||
package rethinkdb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateVersion(t *testing.T) {
|
||||
err := server.validateVersion()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetDefaultTags(t *testing.T) {
|
||||
var tagTests = []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"host", server.Url.Host},
|
||||
{"hostname", server.serverStatus.Network.Hostname},
|
||||
}
|
||||
defaultTags := server.getDefaultTags()
|
||||
for _, tt := range tagTests {
|
||||
if defaultTags[tt.in] != tt.out {
|
||||
t.Errorf("expected %q, got %q", tt.out, defaultTags[tt.in])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClusterStats(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := server.addClusterStats(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, metric := range ClusterTracking {
|
||||
assert.True(t, acc.HasIntValue(metric))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddMemberStats(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := server.addMemberStats(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, metric := range MemberTracking {
|
||||
assert.True(t, acc.HasIntValue(metric))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTableStats(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := server.addTableStats(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, metric := range TableTracking {
|
||||
assert.True(t, acc.HasIntValue(metric))
|
||||
}
|
||||
|
||||
keys := []string{
|
||||
"cache_bytes_in_use",
|
||||
"disk_read_bytes_per_sec",
|
||||
"disk_read_bytes_total",
|
||||
"disk_written_bytes_per_sec",
|
||||
"disk_written_bytes_total",
|
||||
"disk_usage_data_bytes",
|
||||
"disk_usage_garbage_bytes",
|
||||
"disk_usage_metadata_bytes",
|
||||
"disk_usage_preallocated_bytes",
|
||||
}
|
||||
|
||||
for _, metric := range keys {
|
||||
assert.True(t, acc.HasIntValue(metric))
|
||||
}
|
||||
}
|
||||
59
plugins/inputs/rethinkdb/rethinkdb_test.go
Normal file
59
plugins/inputs/rethinkdb/rethinkdb_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// +build integration
|
||||
|
||||
package rethinkdb
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/dancannon/gorethink.v1"
|
||||
)
|
||||
|
||||
var connect_url, authKey string
|
||||
var server *Server
|
||||
|
||||
func init() {
|
||||
connect_url = os.Getenv("RETHINKDB_URL")
|
||||
if connect_url == "" {
|
||||
connect_url = "127.0.0.1:28015"
|
||||
}
|
||||
authKey = os.Getenv("RETHINKDB_AUTHKEY")
|
||||
|
||||
}
|
||||
|
||||
func testSetup(m *testing.M) {
|
||||
var err error
|
||||
server = &Server{Url: &url.URL{Host: connect_url}}
|
||||
server.session, _ = gorethink.Connect(gorethink.ConnectOpts{
|
||||
Address: server.Url.Host,
|
||||
AuthKey: authKey,
|
||||
DiscoverHosts: false,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
|
||||
err = server.getServerStatus()
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func testTeardown(m *testing.M) {
|
||||
server.session.Close()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// seed randomness for use with tests
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
testSetup(m)
|
||||
res := m.Run()
|
||||
testTeardown(m)
|
||||
|
||||
os.Exit(res)
|
||||
}
|
||||
160
plugins/inputs/statsd/README.md
Normal file
160
plugins/inputs/statsd/README.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Telegraf Service Plugin: statsd
|
||||
|
||||
#### Description
|
||||
|
||||
The statsd plugin is a special type of plugin which runs a backgrounded statsd
|
||||
listener service while telegraf is running.
|
||||
|
||||
The format of the statsd messages was based on the format described in the
|
||||
original [etsy statsd](https://github.com/etsy/statsd/blob/master/docs/metric_types.md)
|
||||
implementation. In short, the telegraf statsd listener will accept:
|
||||
|
||||
- Gauges
|
||||
- `users.current.den001.myapp:32|g` <- standard
|
||||
- `users.current.den001.myapp:+10|g` <- additive
|
||||
- `users.current.den001.myapp:-10|g`
|
||||
- Counters
|
||||
- `deploys.test.myservice:1|c` <- increments by 1
|
||||
- `deploys.test.myservice:101|c` <- increments by 101
|
||||
- `deploys.test.myservice:1|c|@0.1` <- with sample rate, increments by 10
|
||||
- Sets
|
||||
- `users.unique:101|s`
|
||||
- `users.unique:101|s`
|
||||
- `users.unique:102|s` <- would result in a count of 2 for `users.unique`
|
||||
- Timings & Histograms
|
||||
- `load.time:320|ms`
|
||||
- `load.time.nanoseconds:1|h`
|
||||
- `load.time:200|ms|@0.1` <- sampled 1/10 of the time
|
||||
|
||||
It is possible to omit repetitive names and merge individual stats into a
|
||||
single line by separating them with additional colons:
|
||||
|
||||
- `users.current.den001.myapp:32|g:+10|g:-10|g`
|
||||
- `deploys.test.myservice:1|c:101|c:1|c|@0.1`
|
||||
- `users.unique:101|s:101|s:102|s`
|
||||
- `load.time:320|ms:200|ms|@0.1`
|
||||
|
||||
This also allows for mixed types in a single line:
|
||||
|
||||
- `foo:1|c:200|ms`
|
||||
|
||||
The string `foo:1|c:200|ms` is internally split into two individual metrics
|
||||
`foo:1|c` and `foo:200|ms` which are added to the aggregator separately.
|
||||
|
||||
|
||||
#### Influx Statsd
|
||||
|
||||
In order to take advantage of InfluxDB's tagging system, we have made a couple
|
||||
additions to the standard statsd protocol. First, you can specify
|
||||
tags in a manner similar to the line-protocol, like this:
|
||||
|
||||
```
|
||||
users.current,service=payroll,region=us-west:32|g
|
||||
```
|
||||
|
||||
COMING SOON: there will be a way to specify multiple fields.
|
||||
<!-- TODO Second, you can specify multiple fields within a measurement:
|
||||
|
||||
```
|
||||
current.users,service=payroll,server=host01:west=10,east=10,central=2,south=10|g
|
||||
``` -->
|
||||
|
||||
#### Measurements:
|
||||
|
||||
Meta:
|
||||
- tags: `metric_type=<gauge|set|counter|timing|histogram>`
|
||||
|
||||
Outputted measurements will depend entirely on the measurements that the user
|
||||
sends, but here is a brief rundown of what you can expect to find from each
|
||||
metric type:
|
||||
|
||||
- Gauges
|
||||
- Gauges are a constant data type. They are not subject to averaging, and they
|
||||
don’t change unless you change them. That is, once you set a gauge value, it
|
||||
will be a flat line on the graph until you change it again.
|
||||
- Counters
|
||||
- Counters are the most basic type. They are treated as a count of a type of
|
||||
event. They will continually increase unless you set `delete_counters=true`.
|
||||
- Sets
|
||||
- Sets count the number of unique values passed to a key. For example, you
|
||||
could count the number of users accessing your system using `users:<user_id>|s`.
|
||||
No matter how many times the same user_id is sent, the count will only increase
|
||||
by 1.
|
||||
- Timings & Histograms
|
||||
- Timers are meant to track how long something took. They are an invaluable
|
||||
tool for tracking application performance.
|
||||
- The following aggregate measurements are made for timers:
|
||||
- `statsd_<name>_lower`: The lower bound is the lowest value statsd saw
|
||||
for that stat during that interval.
|
||||
- `statsd_<name>_upper`: The upper bound is the highest value statsd saw
|
||||
for that stat during that interval.
|
||||
- `statsd_<name>_mean`: The mean is the average of all values statsd saw
|
||||
for that stat during that interval.
|
||||
- `statsd_<name>_stddev`: The stddev is the sample standard deviation
|
||||
of all values statsd saw for that stat during that interval.
|
||||
- `statsd_<name>_count`: The count is the number of timings statsd saw
|
||||
for that stat during that interval. It is not averaged.
|
||||
- `statsd_<name>_percentile_<P>` The `Pth` percentile is a value x such
|
||||
that `P%` of all the values statsd saw for that stat during that time
|
||||
period are below x. The most common value that people use for `P` is the
|
||||
`90`, this is a great number to try to optimize.
|
||||
|
||||
#### Plugin arguments
|
||||
|
||||
- **service_address** string: Address to listen for statsd UDP packets on
|
||||
- **delete_gauges** boolean: Delete gauges on every collection interval
|
||||
- **delete_counters** boolean: Delete counters on every collection interval
|
||||
- **delete_sets** boolean: Delete set counters on every collection interval
|
||||
- **delete_timings** boolean: Delete timings on every collection interval
|
||||
- **percentiles** []int: Percentiles to calculate for timing & histogram stats
|
||||
- **allowed_pending_messages** integer: Number of messages allowed to queue up
|
||||
waiting to be processed. When this fills, messages will be dropped and logged.
|
||||
- **percentile_limit** integer: Number of timing/histogram values to track
|
||||
per-measurement in the calculation of percentiles. Raising this limit increases
|
||||
the accuracy of percentiles but also increases the memory usage and cpu time.
|
||||
- **templates** []string: Templates for transforming statsd buckets into influx
|
||||
measurements and tags.
|
||||
|
||||
#### Statsd bucket -> InfluxDB line-protocol Templates
|
||||
|
||||
The plugin supports specifying templates for transforming statsd buckets into
|
||||
InfluxDB measurement names and tags. The templates have a _measurement_ keyword,
|
||||
which can be used to specify parts of the bucket that are to be used in the
|
||||
measurement name. Other words in the template are used as tag names. For example,
|
||||
the following template:
|
||||
|
||||
```
|
||||
templates = [
|
||||
"measurement.measurement.region"
|
||||
]
|
||||
```
|
||||
|
||||
would result in the following transformation:
|
||||
|
||||
```
|
||||
cpu.load.us-west:100|g
|
||||
=> cpu_load,region=us-west 100
|
||||
```
|
||||
|
||||
Users can also filter the template to use based on the name of the bucket,
|
||||
using glob matching, like so:
|
||||
|
||||
```
|
||||
templates = [
|
||||
"cpu.* measurement.measurement.region",
|
||||
"mem.* measurement.measurement.host"
|
||||
]
|
||||
```
|
||||
|
||||
which would result in the following transformation:
|
||||
|
||||
```
|
||||
cpu.load.us-west:100|g
|
||||
=> cpu_load,region=us-west 100
|
||||
|
||||
mem.cached.localhost:256|g
|
||||
=> mem_cached,host=localhost 256
|
||||
```
|
||||
|
||||
There are many more options available,
|
||||
[More details can be found here](https://github.com/influxdb/influxdb/tree/master/services/graphite#templates)
|
||||
108
plugins/inputs/statsd/running_stats.go
Normal file
108
plugins/inputs/statsd/running_stats.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const defaultPercentileLimit = 1000
|
||||
|
||||
// RunningStats calculates a running mean, variance, standard deviation,
|
||||
// lower bound, upper bound, count, and can calculate estimated percentiles.
|
||||
// It is based on the incremental algorithm described here:
|
||||
// https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
|
||||
type RunningStats struct {
|
||||
k float64
|
||||
n int64
|
||||
ex float64
|
||||
ex2 float64
|
||||
|
||||
// Array used to calculate estimated percentiles
|
||||
// We will store a maximum of PercLimit values, at which point we will start
|
||||
// randomly replacing old values, hence it is an estimated percentile.
|
||||
perc []float64
|
||||
PercLimit int
|
||||
|
||||
upper float64
|
||||
lower float64
|
||||
|
||||
// cache if we have sorted the list so that we never re-sort a sorted list,
|
||||
// which can have very bad performance.
|
||||
sorted bool
|
||||
}
|
||||
|
||||
func (rs *RunningStats) AddValue(v float64) {
|
||||
// Whenever a value is added, the list is no longer sorted.
|
||||
rs.sorted = false
|
||||
|
||||
if rs.n == 0 {
|
||||
rs.k = v
|
||||
rs.upper = v
|
||||
rs.lower = v
|
||||
if rs.PercLimit == 0 {
|
||||
rs.PercLimit = defaultPercentileLimit
|
||||
}
|
||||
rs.perc = make([]float64, 0, rs.PercLimit)
|
||||
}
|
||||
|
||||
// These are used for the running mean and variance
|
||||
rs.n += 1
|
||||
rs.ex += v - rs.k
|
||||
rs.ex2 += (v - rs.k) * (v - rs.k)
|
||||
|
||||
// track upper and lower bounds
|
||||
if v > rs.upper {
|
||||
rs.upper = v
|
||||
} else if v < rs.lower {
|
||||
rs.lower = v
|
||||
}
|
||||
|
||||
if len(rs.perc) < rs.PercLimit {
|
||||
rs.perc = append(rs.perc, v)
|
||||
} else {
|
||||
// Reached limit, choose random index to overwrite in the percentile array
|
||||
rs.perc[rand.Intn(len(rs.perc))] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *RunningStats) Mean() float64 {
|
||||
return rs.k + rs.ex/float64(rs.n)
|
||||
}
|
||||
|
||||
func (rs *RunningStats) Variance() float64 {
|
||||
return (rs.ex2 - (rs.ex*rs.ex)/float64(rs.n)) / float64(rs.n)
|
||||
}
|
||||
|
||||
func (rs *RunningStats) Stddev() float64 {
|
||||
return math.Sqrt(rs.Variance())
|
||||
}
|
||||
|
||||
func (rs *RunningStats) Upper() float64 {
|
||||
return rs.upper
|
||||
}
|
||||
|
||||
func (rs *RunningStats) Lower() float64 {
|
||||
return rs.lower
|
||||
}
|
||||
|
||||
func (rs *RunningStats) Count() int64 {
|
||||
return rs.n
|
||||
}
|
||||
|
||||
func (rs *RunningStats) Percentile(n int) float64 {
|
||||
if n > 100 {
|
||||
n = 100
|
||||
}
|
||||
|
||||
if !rs.sorted {
|
||||
sort.Float64s(rs.perc)
|
||||
rs.sorted = true
|
||||
}
|
||||
|
||||
i := int(float64(len(rs.perc)) * float64(n) / float64(100))
|
||||
if i < 0 {
|
||||
i = 0
|
||||
}
|
||||
return rs.perc[i]
|
||||
}
|
||||
136
plugins/inputs/statsd/running_stats_test.go
Normal file
136
plugins/inputs/statsd/running_stats_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test that a single metric is handled correctly
|
||||
func TestRunningStats_Single(t *testing.T) {
|
||||
rs := RunningStats{}
|
||||
values := []float64{10.1}
|
||||
|
||||
for _, v := range values {
|
||||
rs.AddValue(v)
|
||||
}
|
||||
|
||||
if rs.Mean() != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Mean())
|
||||
}
|
||||
if rs.Upper() != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Upper())
|
||||
}
|
||||
if rs.Lower() != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Lower())
|
||||
}
|
||||
if rs.Percentile(90) != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Percentile(90))
|
||||
}
|
||||
if rs.Percentile(50) != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Percentile(50))
|
||||
}
|
||||
if rs.Count() != 1 {
|
||||
t.Errorf("Expected %v, got %v", 1, rs.Count())
|
||||
}
|
||||
if rs.Variance() != 0 {
|
||||
t.Errorf("Expected %v, got %v", 0, rs.Variance())
|
||||
}
|
||||
if rs.Stddev() != 0 {
|
||||
t.Errorf("Expected %v, got %v", 0, rs.Stddev())
|
||||
}
|
||||
}
|
||||
|
||||
// Test that duplicate values are handled correctly
|
||||
func TestRunningStats_Duplicate(t *testing.T) {
|
||||
rs := RunningStats{}
|
||||
values := []float64{10.1, 10.1, 10.1, 10.1}
|
||||
|
||||
for _, v := range values {
|
||||
rs.AddValue(v)
|
||||
}
|
||||
|
||||
if rs.Mean() != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Mean())
|
||||
}
|
||||
if rs.Upper() != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Upper())
|
||||
}
|
||||
if rs.Lower() != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Lower())
|
||||
}
|
||||
if rs.Percentile(90) != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Percentile(90))
|
||||
}
|
||||
if rs.Percentile(50) != 10.1 {
|
||||
t.Errorf("Expected %v, got %v", 10.1, rs.Percentile(50))
|
||||
}
|
||||
if rs.Count() != 4 {
|
||||
t.Errorf("Expected %v, got %v", 4, rs.Count())
|
||||
}
|
||||
if rs.Variance() != 0 {
|
||||
t.Errorf("Expected %v, got %v", 0, rs.Variance())
|
||||
}
|
||||
if rs.Stddev() != 0 {
|
||||
t.Errorf("Expected %v, got %v", 0, rs.Stddev())
|
||||
}
|
||||
}
|
||||
|
||||
// Test a list of sample values, returns all correct values
|
||||
func TestRunningStats(t *testing.T) {
|
||||
rs := RunningStats{}
|
||||
values := []float64{10, 20, 10, 30, 20, 11, 12, 32, 45, 9, 5, 5, 5, 10, 23, 8}
|
||||
|
||||
for _, v := range values {
|
||||
rs.AddValue(v)
|
||||
}
|
||||
|
||||
if rs.Mean() != 15.9375 {
|
||||
t.Errorf("Expected %v, got %v", 15.9375, rs.Mean())
|
||||
}
|
||||
if rs.Upper() != 45 {
|
||||
t.Errorf("Expected %v, got %v", 45, rs.Upper())
|
||||
}
|
||||
if rs.Lower() != 5 {
|
||||
t.Errorf("Expected %v, got %v", 5, rs.Lower())
|
||||
}
|
||||
if rs.Percentile(90) != 32 {
|
||||
t.Errorf("Expected %v, got %v", 32, rs.Percentile(90))
|
||||
}
|
||||
if rs.Percentile(50) != 11 {
|
||||
t.Errorf("Expected %v, got %v", 11, rs.Percentile(50))
|
||||
}
|
||||
if rs.Count() != 16 {
|
||||
t.Errorf("Expected %v, got %v", 4, rs.Count())
|
||||
}
|
||||
if !fuzzyEqual(rs.Variance(), 124.93359, .00001) {
|
||||
t.Errorf("Expected %v, got %v", 124.93359, rs.Variance())
|
||||
}
|
||||
if !fuzzyEqual(rs.Stddev(), 11.17736, .00001) {
|
||||
t.Errorf("Expected %v, got %v", 11.17736, rs.Stddev())
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the percentile limit is respected.
|
||||
func TestRunningStats_PercentileLimit(t *testing.T) {
|
||||
rs := RunningStats{}
|
||||
rs.PercLimit = 10
|
||||
values := []float64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
|
||||
|
||||
for _, v := range values {
|
||||
rs.AddValue(v)
|
||||
}
|
||||
|
||||
if rs.Count() != 11 {
|
||||
t.Errorf("Expected %v, got %v", 11, rs.Count())
|
||||
}
|
||||
if len(rs.perc) != 10 {
|
||||
t.Errorf("Expected %v, got %v", 10, len(rs.perc))
|
||||
}
|
||||
}
|
||||
|
||||
func fuzzyEqual(a, b, epsilon float64) bool {
|
||||
if math.Abs(a-b) > epsilon {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
496
plugins/inputs/statsd/statsd.go
Normal file
496
plugins/inputs/statsd/statsd.go
Normal file
@@ -0,0 +1,496 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdb/influxdb/services/graphite"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
var dropwarn = "ERROR: Message queue full. Discarding line [%s] " +
|
||||
"You may want to increase allowed_pending_messages in the config\n"
|
||||
|
||||
type Statsd struct {
|
||||
// Address & Port to serve from
|
||||
ServiceAddress string
|
||||
|
||||
// Number of messages allowed to queue up in between calls to Gather. If this
|
||||
// fills up, packets will get dropped until the next Gather interval is ran.
|
||||
AllowedPendingMessages int
|
||||
|
||||
// Percentiles specifies the percentiles that will be calculated for timing
|
||||
// and histogram stats.
|
||||
Percentiles []int
|
||||
PercentileLimit int
|
||||
|
||||
DeleteGauges bool
|
||||
DeleteCounters bool
|
||||
DeleteSets bool
|
||||
DeleteTimings bool
|
||||
|
||||
sync.Mutex
|
||||
|
||||
// Channel for all incoming statsd messages
|
||||
in chan string
|
||||
done chan struct{}
|
||||
|
||||
// Cache gauges, counters & sets so they can be aggregated as they arrive
|
||||
gauges map[string]cachedgauge
|
||||
counters map[string]cachedcounter
|
||||
sets map[string]cachedset
|
||||
timings map[string]cachedtimings
|
||||
|
||||
// bucket -> influx templates
|
||||
Templates []string
|
||||
}
|
||||
|
||||
func NewStatsd() *Statsd {
|
||||
s := Statsd{}
|
||||
|
||||
// Make data structures
|
||||
s.done = make(chan struct{})
|
||||
s.in = make(chan string, s.AllowedPendingMessages)
|
||||
s.gauges = make(map[string]cachedgauge)
|
||||
s.counters = make(map[string]cachedcounter)
|
||||
s.sets = make(map[string]cachedset)
|
||||
s.timings = make(map[string]cachedtimings)
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
// One statsd metric, form is <bucket>:<value>|<mtype>|@<samplerate>
|
||||
type metric struct {
|
||||
name string
|
||||
bucket string
|
||||
hash string
|
||||
intvalue int64
|
||||
floatvalue float64
|
||||
mtype string
|
||||
additive bool
|
||||
samplerate float64
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
type cachedset struct {
|
||||
name string
|
||||
set map[int64]bool
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
type cachedgauge struct {
|
||||
name string
|
||||
value float64
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
type cachedcounter struct {
|
||||
name string
|
||||
value int64
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
type cachedtimings struct {
|
||||
name string
|
||||
stats RunningStats
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
func (_ *Statsd) Description() string {
|
||||
return "Statsd Server"
|
||||
}
|
||||
|
||||
const sampleConfig = `
|
||||
# Address and port to host UDP listener on
|
||||
service_address = ":8125"
|
||||
# Delete gauges every interval (default=false)
|
||||
delete_gauges = false
|
||||
# Delete counters every interval (default=false)
|
||||
delete_counters = false
|
||||
# Delete sets every interval (default=false)
|
||||
delete_sets = false
|
||||
# Delete timings & histograms every interval (default=true)
|
||||
delete_timings = true
|
||||
# Percentiles to calculate for timing & histogram stats
|
||||
percentiles = [90]
|
||||
|
||||
# templates = [
|
||||
# "cpu.* measurement*"
|
||||
# ]
|
||||
|
||||
# Number of UDP messages allowed to queue up, once filled,
|
||||
# the statsd server will start dropping packets
|
||||
allowed_pending_messages = 10000
|
||||
|
||||
# Number of timing/histogram values to track per-measurement in the
|
||||
# calculation of percentiles. Raising this limit increases the accuracy
|
||||
# of percentiles but also increases the memory usage and cpu time.
|
||||
percentile_limit = 1000
|
||||
`
|
||||
|
||||
func (_ *Statsd) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (s *Statsd) Gather(acc inputs.Accumulator) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
for _, metric := range s.timings {
|
||||
acc.Add(metric.name+"_mean", metric.stats.Mean(), metric.tags)
|
||||
acc.Add(metric.name+"_stddev", metric.stats.Stddev(), metric.tags)
|
||||
acc.Add(metric.name+"_upper", metric.stats.Upper(), metric.tags)
|
||||
acc.Add(metric.name+"_lower", metric.stats.Lower(), metric.tags)
|
||||
acc.Add(metric.name+"_count", metric.stats.Count(), metric.tags)
|
||||
for _, percentile := range s.Percentiles {
|
||||
name := fmt.Sprintf("%s_percentile_%v", metric.name, percentile)
|
||||
acc.Add(name, metric.stats.Percentile(percentile), metric.tags)
|
||||
}
|
||||
}
|
||||
if s.DeleteTimings {
|
||||
s.timings = make(map[string]cachedtimings)
|
||||
}
|
||||
|
||||
for _, metric := range s.gauges {
|
||||
acc.Add(metric.name, metric.value, metric.tags)
|
||||
}
|
||||
if s.DeleteGauges {
|
||||
s.gauges = make(map[string]cachedgauge)
|
||||
}
|
||||
|
||||
for _, metric := range s.counters {
|
||||
acc.Add(metric.name, metric.value, metric.tags)
|
||||
}
|
||||
if s.DeleteCounters {
|
||||
s.counters = make(map[string]cachedcounter)
|
||||
}
|
||||
|
||||
for _, metric := range s.sets {
|
||||
acc.Add(metric.name, int64(len(metric.set)), metric.tags)
|
||||
}
|
||||
if s.DeleteSets {
|
||||
s.sets = make(map[string]cachedset)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Statsd) Start() error {
|
||||
// Make data structures
|
||||
s.done = make(chan struct{})
|
||||
s.in = make(chan string, s.AllowedPendingMessages)
|
||||
s.gauges = make(map[string]cachedgauge)
|
||||
s.counters = make(map[string]cachedcounter)
|
||||
s.sets = make(map[string]cachedset)
|
||||
s.timings = make(map[string]cachedtimings)
|
||||
|
||||
// Start the UDP listener
|
||||
go s.udpListen()
|
||||
// Start the line parser
|
||||
go s.parser()
|
||||
log.Printf("Started the statsd service on %s\n", s.ServiceAddress)
|
||||
return nil
|
||||
}
|
||||
|
||||
// udpListen starts listening for udp packets on the configured port.
|
||||
func (s *Statsd) udpListen() error {
|
||||
address, _ := net.ResolveUDPAddr("udp", s.ServiceAddress)
|
||||
listener, err := net.ListenUDP("udp", address)
|
||||
if err != nil {
|
||||
log.Fatalf("ERROR: ListenUDP - %s", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
log.Println("Statsd listener listening on: ", listener.LocalAddr().String())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return nil
|
||||
default:
|
||||
buf := make([]byte, 1024)
|
||||
n, _, err := listener.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: %s\n", err.Error())
|
||||
}
|
||||
|
||||
lines := strings.Split(string(buf[:n]), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
select {
|
||||
case s.in <- line:
|
||||
default:
|
||||
log.Printf(dropwarn, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parser monitors the s.in channel, if there is a line ready, it parses the
|
||||
// statsd string into a usable metric struct and aggregates the value
|
||||
func (s *Statsd) parser() error {
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return nil
|
||||
case line := <-s.in:
|
||||
s.parseStatsdLine(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseStatsdLine will parse the given statsd line, validating it as it goes.
|
||||
// If the line is valid, it will be cached for the next call to Gather()
|
||||
func (s *Statsd) parseStatsdLine(line string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
// Validate splitting the line on ":"
|
||||
bits := strings.Split(line, ":")
|
||||
if len(bits) < 2 {
|
||||
log.Printf("Error: splitting ':', Unable to parse metric: %s\n", line)
|
||||
return errors.New("Error Parsing statsd line")
|
||||
}
|
||||
|
||||
// Extract bucket name from individual metric bits
|
||||
bucketName, bits := bits[0], bits[1:]
|
||||
|
||||
// Add a metric for each bit available
|
||||
for _, bit := range bits {
|
||||
m := metric{}
|
||||
|
||||
m.bucket = bucketName
|
||||
|
||||
// Validate splitting the bit on "|"
|
||||
pipesplit := strings.Split(bit, "|")
|
||||
if len(pipesplit) < 2 {
|
||||
log.Printf("Error: splitting '|', Unable to parse metric: %s\n", line)
|
||||
return errors.New("Error Parsing statsd line")
|
||||
} else if len(pipesplit) > 2 {
|
||||
sr := pipesplit[2]
|
||||
errmsg := "Error: parsing sample rate, %s, it must be in format like: " +
|
||||
"@0.1, @0.5, etc. Ignoring sample rate for line: %s\n"
|
||||
if strings.Contains(sr, "@") && len(sr) > 1 {
|
||||
samplerate, err := strconv.ParseFloat(sr[1:], 64)
|
||||
if err != nil {
|
||||
log.Printf(errmsg, err.Error(), line)
|
||||
} else {
|
||||
// sample rate successfully parsed
|
||||
m.samplerate = samplerate
|
||||
}
|
||||
} else {
|
||||
log.Printf(errmsg, "", line)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate metric type
|
||||
switch pipesplit[1] {
|
||||
case "g", "c", "s", "ms", "h":
|
||||
m.mtype = pipesplit[1]
|
||||
default:
|
||||
log.Printf("Error: Statsd Metric type %s unsupported", pipesplit[1])
|
||||
return errors.New("Error Parsing statsd line")
|
||||
}
|
||||
|
||||
// Parse the value
|
||||
if strings.ContainsAny(pipesplit[0], "-+") {
|
||||
if m.mtype != "g" {
|
||||
log.Printf("Error: +- values are only supported for gauges: %s\n", line)
|
||||
return errors.New("Error Parsing statsd line")
|
||||
}
|
||||
m.additive = true
|
||||
}
|
||||
|
||||
switch m.mtype {
|
||||
case "g", "ms", "h":
|
||||
v, err := strconv.ParseFloat(pipesplit[0], 64)
|
||||
if err != nil {
|
||||
log.Printf("Error: parsing value to float64: %s\n", line)
|
||||
return errors.New("Error Parsing statsd line")
|
||||
}
|
||||
m.floatvalue = v
|
||||
case "c", "s":
|
||||
v, err := strconv.ParseInt(pipesplit[0], 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("Error: parsing value to int64: %s\n", line)
|
||||
return errors.New("Error Parsing statsd line")
|
||||
}
|
||||
// If a sample rate is given with a counter, divide value by the rate
|
||||
if m.samplerate != 0 && m.mtype == "c" {
|
||||
v = int64(float64(v) / m.samplerate)
|
||||
}
|
||||
m.intvalue = v
|
||||
}
|
||||
|
||||
// Parse the name & tags from bucket
|
||||
m.name, m.tags = s.parseName(m.bucket)
|
||||
switch m.mtype {
|
||||
case "c":
|
||||
m.tags["metric_type"] = "counter"
|
||||
case "g":
|
||||
m.tags["metric_type"] = "gauge"
|
||||
case "s":
|
||||
m.tags["metric_type"] = "set"
|
||||
case "ms":
|
||||
m.tags["metric_type"] = "timing"
|
||||
case "h":
|
||||
m.tags["metric_type"] = "histogram"
|
||||
}
|
||||
|
||||
// Make a unique key for the measurement name/tags
|
||||
var tg []string
|
||||
for k, v := range m.tags {
|
||||
tg = append(tg, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
sort.Strings(tg)
|
||||
m.hash = fmt.Sprintf("%s%s", strings.Join(tg, ""), m.name)
|
||||
|
||||
s.aggregate(m)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseName parses the given bucket name with the list of bucket maps in the
|
||||
// config file. If there is a match, it will parse the name of the metric and
|
||||
// map of tags.
|
||||
// Return values are (<name>, <tags>)
|
||||
func (s *Statsd) parseName(bucket string) (string, map[string]string) {
|
||||
tags := make(map[string]string)
|
||||
|
||||
bucketparts := strings.Split(bucket, ",")
|
||||
// Parse out any tags in the bucket
|
||||
if len(bucketparts) > 1 {
|
||||
for _, btag := range bucketparts[1:] {
|
||||
k, v := parseKeyValue(btag)
|
||||
if k != "" {
|
||||
tags[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
o := graphite.Options{
|
||||
Separator: "_",
|
||||
Templates: s.Templates,
|
||||
DefaultTags: tags,
|
||||
}
|
||||
|
||||
name := bucketparts[0]
|
||||
p, err := graphite.NewParserWithOptions(o)
|
||||
if err == nil {
|
||||
name, tags, _, _ = p.ApplyTemplate(name)
|
||||
}
|
||||
name = strings.Replace(name, ".", "_", -1)
|
||||
name = strings.Replace(name, "-", "__", -1)
|
||||
|
||||
return name, tags
|
||||
}
|
||||
|
||||
// Parse the key,value out of a string that looks like "key=value"
|
||||
func parseKeyValue(keyvalue string) (string, string) {
|
||||
var key, val string
|
||||
|
||||
split := strings.Split(keyvalue, "=")
|
||||
// Must be exactly 2 to get anything meaningful out of them
|
||||
if len(split) == 2 {
|
||||
key = split[0]
|
||||
val = split[1]
|
||||
} else if len(split) == 1 {
|
||||
val = split[0]
|
||||
}
|
||||
|
||||
return key, val
|
||||
}
|
||||
|
||||
// aggregate takes in a metric. It then
|
||||
// aggregates and caches the current value(s). It does not deal with the
|
||||
// Delete* options, because those are dealt with in the Gather function.
|
||||
func (s *Statsd) aggregate(m metric) {
|
||||
switch m.mtype {
|
||||
case "ms", "h":
|
||||
cached, ok := s.timings[m.hash]
|
||||
if !ok {
|
||||
cached = cachedtimings{
|
||||
name: m.name,
|
||||
tags: m.tags,
|
||||
stats: RunningStats{
|
||||
PercLimit: s.PercentileLimit,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if m.samplerate > 0 {
|
||||
for i := 0; i < int(1.0/m.samplerate); i++ {
|
||||
cached.stats.AddValue(m.floatvalue)
|
||||
}
|
||||
s.timings[m.hash] = cached
|
||||
} else {
|
||||
cached.stats.AddValue(m.floatvalue)
|
||||
s.timings[m.hash] = cached
|
||||
}
|
||||
case "c":
|
||||
cached, ok := s.counters[m.hash]
|
||||
if !ok {
|
||||
s.counters[m.hash] = cachedcounter{
|
||||
name: m.name,
|
||||
value: m.intvalue,
|
||||
tags: m.tags,
|
||||
}
|
||||
} else {
|
||||
cached.value += m.intvalue
|
||||
s.counters[m.hash] = cached
|
||||
}
|
||||
case "g":
|
||||
cached, ok := s.gauges[m.hash]
|
||||
if !ok {
|
||||
s.gauges[m.hash] = cachedgauge{
|
||||
name: m.name,
|
||||
value: m.floatvalue,
|
||||
tags: m.tags,
|
||||
}
|
||||
} else {
|
||||
if m.additive {
|
||||
cached.value = cached.value + m.floatvalue
|
||||
} else {
|
||||
cached.value = m.floatvalue
|
||||
}
|
||||
s.gauges[m.hash] = cached
|
||||
}
|
||||
case "s":
|
||||
cached, ok := s.sets[m.hash]
|
||||
if !ok {
|
||||
// Completely new metric (initialize with count of 1)
|
||||
s.sets[m.hash] = cachedset{
|
||||
name: m.name,
|
||||
tags: m.tags,
|
||||
set: map[int64]bool{m.intvalue: true},
|
||||
}
|
||||
} else {
|
||||
cached.set[m.intvalue] = true
|
||||
s.sets[m.hash] = cached
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Statsd) Stop() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
log.Println("Stopping the statsd service")
|
||||
close(s.done)
|
||||
close(s.in)
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("statsd", func() inputs.Input {
|
||||
return &Statsd{}
|
||||
})
|
||||
}
|
||||
900
plugins/inputs/statsd/statsd_test.go
Normal file
900
plugins/inputs/statsd/statsd_test.go
Normal file
@@ -0,0 +1,900 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
)
|
||||
|
||||
// Invalid lines should return an error
|
||||
func TestParse_InvalidLines(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
invalid_lines := []string{
|
||||
"i.dont.have.a.pipe:45g",
|
||||
"i.dont.have.a.colon45|c",
|
||||
"invalid.metric.type:45|e",
|
||||
"invalid.plus.minus.non.gauge:+10|c",
|
||||
"invalid.plus.minus.non.gauge:+10|s",
|
||||
"invalid.plus.minus.non.gauge:+10|ms",
|
||||
"invalid.plus.minus.non.gauge:+10|h",
|
||||
"invalid.plus.minus.non.gauge:-10|c",
|
||||
"invalid.value:foobar|c",
|
||||
"invalid.value:d11|c",
|
||||
"invalid.value:1d1|c",
|
||||
}
|
||||
for _, line := range invalid_lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err == nil {
|
||||
t.Errorf("Parsing line %s should have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid sample rates should be ignored and not applied
|
||||
func TestParse_InvalidSampleRate(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
invalid_lines := []string{
|
||||
"invalid.sample.rate:45|c|0.1",
|
||||
"invalid.sample.rate.2:45|c|@foo",
|
||||
"invalid.sample.rate:45|g|@0.1",
|
||||
"invalid.sample.rate:45|s|@0.1",
|
||||
}
|
||||
|
||||
for _, line := range invalid_lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
counter_validations := []struct {
|
||||
name string
|
||||
value int64
|
||||
cache map[string]cachedcounter
|
||||
}{
|
||||
{
|
||||
"invalid_sample_rate",
|
||||
45,
|
||||
s.counters,
|
||||
},
|
||||
{
|
||||
"invalid_sample_rate_2",
|
||||
45,
|
||||
s.counters,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range counter_validations {
|
||||
err := test_validate_counter(test.name, test.value, test.cache)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
err := test_validate_gauge("invalid_sample_rate", 45, s.gauges)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
err = test_validate_set("invalid_sample_rate", 1, s.sets)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Names should be parsed like . -> _ and - -> __
|
||||
func TestParse_DefaultNameParsing(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
valid_lines := []string{
|
||||
"valid:1|c",
|
||||
"valid.foo-bar:11|c",
|
||||
}
|
||||
|
||||
for _, line := range valid_lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
validations := []struct {
|
||||
name string
|
||||
value int64
|
||||
}{
|
||||
{
|
||||
"valid",
|
||||
1,
|
||||
},
|
||||
{
|
||||
"valid_foo__bar",
|
||||
11,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range validations {
|
||||
err := test_validate_counter(test.name, test.value, s.counters)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that template name transformation works
|
||||
func TestParse_Template(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
s.Templates = []string{
|
||||
"measurement.measurement.host.service",
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"cpu.idle.localhost:1|c",
|
||||
"cpu.busy.host01.myservice:11|c",
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
validations := []struct {
|
||||
name string
|
||||
value int64
|
||||
}{
|
||||
{
|
||||
"cpu_idle",
|
||||
1,
|
||||
},
|
||||
{
|
||||
"cpu_busy",
|
||||
11,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate counters
|
||||
for _, test := range validations {
|
||||
err := test_validate_counter(test.name, test.value, s.counters)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that template filters properly
|
||||
func TestParse_TemplateFilter(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
s.Templates = []string{
|
||||
"cpu.idle.* measurement.measurement.host",
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"cpu.idle.localhost:1|c",
|
||||
"cpu.busy.host01.myservice:11|c",
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
validations := []struct {
|
||||
name string
|
||||
value int64
|
||||
}{
|
||||
{
|
||||
"cpu_idle",
|
||||
1,
|
||||
},
|
||||
{
|
||||
"cpu_busy_host01_myservice",
|
||||
11,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate counters
|
||||
for _, test := range validations {
|
||||
err := test_validate_counter(test.name, test.value, s.counters)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that most specific template is chosen
|
||||
func TestParse_TemplateSpecificity(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
s.Templates = []string{
|
||||
"cpu.* measurement.foo.host",
|
||||
"cpu.idle.* measurement.measurement.host",
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"cpu.idle.localhost:1|c",
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
validations := []struct {
|
||||
name string
|
||||
value int64
|
||||
}{
|
||||
{
|
||||
"cpu_idle",
|
||||
1,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate counters
|
||||
for _, test := range validations {
|
||||
err := test_validate_counter(test.name, test.value, s.counters)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that fields are parsed correctly
|
||||
func TestParse_Fields(t *testing.T) {
|
||||
if false {
|
||||
t.Errorf("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that tags within the bucket are parsed correctly
|
||||
func TestParse_Tags(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
|
||||
tests := []struct {
|
||||
bucket string
|
||||
name string
|
||||
tags map[string]string
|
||||
}{
|
||||
{
|
||||
"cpu.idle,host=localhost",
|
||||
"cpu_idle",
|
||||
map[string]string{
|
||||
"host": "localhost",
|
||||
},
|
||||
},
|
||||
{
|
||||
"cpu.idle,host=localhost,region=west",
|
||||
"cpu_idle",
|
||||
map[string]string{
|
||||
"host": "localhost",
|
||||
"region": "west",
|
||||
},
|
||||
},
|
||||
{
|
||||
"cpu.idle,host=localhost,color=red,region=west",
|
||||
"cpu_idle",
|
||||
map[string]string{
|
||||
"host": "localhost",
|
||||
"region": "west",
|
||||
"color": "red",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
name, tags := s.parseName(test.bucket)
|
||||
if name != test.name {
|
||||
t.Errorf("Expected: %s, got %s", test.name, name)
|
||||
}
|
||||
|
||||
for k, v := range test.tags {
|
||||
actual, ok := tags[k]
|
||||
if !ok {
|
||||
t.Errorf("Expected key: %s not found", k)
|
||||
}
|
||||
if actual != v {
|
||||
t.Errorf("Expected %s, got %s", v, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that measurements with the same name, but different tags, are treated
|
||||
// as different outputs
|
||||
func TestParse_MeasurementsWithSameName(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
|
||||
// Test that counters work
|
||||
valid_lines := []string{
|
||||
"test.counter,host=localhost:1|c",
|
||||
"test.counter,host=localhost,region=west:1|c",
|
||||
}
|
||||
|
||||
for _, line := range valid_lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.counters) != 2 {
|
||||
t.Errorf("Expected 2 separate measurements, found %d", len(s.counters))
|
||||
}
|
||||
}
|
||||
|
||||
// Test that measurements with multiple bits, are treated as different outputs
|
||||
// but are equal to their single-measurement representation
|
||||
func TestParse_MeasurementsWithMultipleValues(t *testing.T) {
|
||||
single_lines := []string{
|
||||
"valid.multiple:0|ms|@0.1",
|
||||
"valid.multiple:0|ms|",
|
||||
"valid.multiple:1|ms",
|
||||
"valid.multiple.duplicate:1|c",
|
||||
"valid.multiple.duplicate:1|c",
|
||||
"valid.multiple.duplicate:2|c",
|
||||
"valid.multiple.duplicate:1|c",
|
||||
"valid.multiple.duplicate:1|h",
|
||||
"valid.multiple.duplicate:1|h",
|
||||
"valid.multiple.duplicate:2|h",
|
||||
"valid.multiple.duplicate:1|h",
|
||||
"valid.multiple.duplicate:1|s",
|
||||
"valid.multiple.duplicate:1|s",
|
||||
"valid.multiple.duplicate:2|s",
|
||||
"valid.multiple.duplicate:1|s",
|
||||
"valid.multiple.duplicate:1|g",
|
||||
"valid.multiple.duplicate:1|g",
|
||||
"valid.multiple.duplicate:2|g",
|
||||
"valid.multiple.duplicate:1|g",
|
||||
"valid.multiple.mixed:1|c",
|
||||
"valid.multiple.mixed:1|ms",
|
||||
"valid.multiple.mixed:2|s",
|
||||
"valid.multiple.mixed:1|g",
|
||||
}
|
||||
|
||||
multiple_lines := []string{
|
||||
"valid.multiple:0|ms|@0.1:0|ms|:1|ms",
|
||||
"valid.multiple.duplicate:1|c:1|c:2|c:1|c",
|
||||
"valid.multiple.duplicate:1|h:1|h:2|h:1|h",
|
||||
"valid.multiple.duplicate:1|s:1|s:2|s:1|s",
|
||||
"valid.multiple.duplicate:1|g:1|g:2|g:1|g",
|
||||
"valid.multiple.mixed:1|c:1|ms:2|s:1|g",
|
||||
}
|
||||
|
||||
s_single := NewStatsd()
|
||||
s_multiple := NewStatsd()
|
||||
|
||||
for _, line := range single_lines {
|
||||
err := s_single.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
for _, line := range multiple_lines {
|
||||
err := s_multiple.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
if len(s_single.timings) != 3 {
|
||||
t.Errorf("Expected 3 measurement, found %d", len(s_single.timings))
|
||||
}
|
||||
|
||||
if cachedtiming, ok := s_single.timings["metric_type=timingvalid_multiple"]; !ok {
|
||||
t.Errorf("Expected cached measurement with hash 'metric_type=timingvalid_multiple' not found")
|
||||
} else {
|
||||
if cachedtiming.name != "valid_multiple" {
|
||||
t.Errorf("Expected the name to be 'valid_multiple', got %s", cachedtiming.name)
|
||||
}
|
||||
|
||||
// A 0 at samplerate 0.1 will add 10 values of 0,
|
||||
// A 0 with invalid samplerate will add a single 0,
|
||||
// plus the last bit of value 1
|
||||
// which adds up to 12 individual datapoints to be cached
|
||||
if cachedtiming.stats.n != 12 {
|
||||
t.Errorf("Expected 11 additions, got %d", cachedtiming.stats.n)
|
||||
}
|
||||
|
||||
if cachedtiming.stats.upper != 1 {
|
||||
t.Errorf("Expected max input to be 1, got %f", cachedtiming.stats.upper)
|
||||
}
|
||||
}
|
||||
|
||||
// test if s_single and s_multiple did compute the same stats for valid.multiple.duplicate
|
||||
if err := test_validate_set("valid_multiple_duplicate", 2, s_single.sets); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_set("valid_multiple_duplicate", 2, s_multiple.sets); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_counter("valid_multiple_duplicate", 5, s_single.counters); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_counter("valid_multiple_duplicate", 5, s_multiple.counters); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_gauge("valid_multiple_duplicate", 1, s_single.gauges); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_gauge("valid_multiple_duplicate", 1, s_multiple.gauges); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
// test if s_single and s_multiple did compute the same stats for valid.multiple.mixed
|
||||
if err := test_validate_set("valid_multiple_mixed", 1, s_single.sets); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_set("valid_multiple_mixed", 1, s_multiple.sets); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_counter("valid_multiple_mixed", 1, s_single.counters); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_counter("valid_multiple_mixed", 1, s_multiple.counters); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_gauge("valid_multiple_mixed", 1, s_single.gauges); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := test_validate_gauge("valid_multiple_mixed", 1, s_multiple.gauges); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Valid lines should be parsed and their values should be cached
|
||||
func TestParse_ValidLines(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
valid_lines := []string{
|
||||
"valid:45|c",
|
||||
"valid:45|s",
|
||||
"valid:45|g",
|
||||
"valid.timer:45|ms",
|
||||
"valid.timer:45|h",
|
||||
}
|
||||
|
||||
for _, line := range valid_lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests low-level functionality of gauges
|
||||
func TestParse_Gauges(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
|
||||
// Test that gauge +- values work
|
||||
valid_lines := []string{
|
||||
"plus.minus:100|g",
|
||||
"plus.minus:-10|g",
|
||||
"plus.minus:+30|g",
|
||||
"plus.plus:100|g",
|
||||
"plus.plus:+100|g",
|
||||
"plus.plus:+100|g",
|
||||
"minus.minus:100|g",
|
||||
"minus.minus:-100|g",
|
||||
"minus.minus:-100|g",
|
||||
"lone.plus:+100|g",
|
||||
"lone.minus:-100|g",
|
||||
"overwrite:100|g",
|
||||
"overwrite:300|g",
|
||||
}
|
||||
|
||||
for _, line := range valid_lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
validations := []struct {
|
||||
name string
|
||||
value float64
|
||||
}{
|
||||
{
|
||||
"plus_minus",
|
||||
120,
|
||||
},
|
||||
{
|
||||
"plus_plus",
|
||||
300,
|
||||
},
|
||||
{
|
||||
"minus_minus",
|
||||
-100,
|
||||
},
|
||||
{
|
||||
"lone_plus",
|
||||
100,
|
||||
},
|
||||
{
|
||||
"lone_minus",
|
||||
-100,
|
||||
},
|
||||
{
|
||||
"overwrite",
|
||||
300,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range validations {
|
||||
err := test_validate_gauge(test.name, test.value, s.gauges)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests low-level functionality of sets
|
||||
func TestParse_Sets(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
|
||||
// Test that sets work
|
||||
valid_lines := []string{
|
||||
"unique.user.ids:100|s",
|
||||
"unique.user.ids:100|s",
|
||||
"unique.user.ids:100|s",
|
||||
"unique.user.ids:100|s",
|
||||
"unique.user.ids:100|s",
|
||||
"unique.user.ids:101|s",
|
||||
"unique.user.ids:102|s",
|
||||
"unique.user.ids:102|s",
|
||||
"unique.user.ids:123456789|s",
|
||||
"oneuser.id:100|s",
|
||||
"oneuser.id:100|s",
|
||||
}
|
||||
|
||||
for _, line := range valid_lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
validations := []struct {
|
||||
name string
|
||||
value int64
|
||||
}{
|
||||
{
|
||||
"unique_user_ids",
|
||||
4,
|
||||
},
|
||||
{
|
||||
"oneuser_id",
|
||||
1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range validations {
|
||||
err := test_validate_set(test.name, test.value, s.sets)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests low-level functionality of counters
|
||||
func TestParse_Counters(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
|
||||
// Test that counters work
|
||||
valid_lines := []string{
|
||||
"small.inc:1|c",
|
||||
"big.inc:100|c",
|
||||
"big.inc:1|c",
|
||||
"big.inc:100000|c",
|
||||
"big.inc:1000000|c",
|
||||
"small.inc:1|c",
|
||||
"zero.init:0|c",
|
||||
"sample.rate:1|c|@0.1",
|
||||
"sample.rate:1|c",
|
||||
}
|
||||
|
||||
for _, line := range valid_lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
validations := []struct {
|
||||
name string
|
||||
value int64
|
||||
}{
|
||||
{
|
||||
"small_inc",
|
||||
2,
|
||||
},
|
||||
{
|
||||
"big_inc",
|
||||
1100101,
|
||||
},
|
||||
{
|
||||
"zero_init",
|
||||
0,
|
||||
},
|
||||
{
|
||||
"sample_rate",
|
||||
11,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range validations {
|
||||
err := test_validate_counter(test.name, test.value, s.counters)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests low-level functionality of timings
|
||||
func TestParse_Timings(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
s.Percentiles = []int{90}
|
||||
acc := &testutil.Accumulator{}
|
||||
|
||||
// Test that counters work
|
||||
valid_lines := []string{
|
||||
"test.timing:1|ms",
|
||||
"test.timing:1|ms",
|
||||
"test.timing:1|ms",
|
||||
"test.timing:1|ms",
|
||||
"test.timing:1|ms",
|
||||
}
|
||||
|
||||
for _, line := range valid_lines {
|
||||
err := s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
s.Gather(acc)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value interface{}
|
||||
}{
|
||||
{
|
||||
"test_timing_mean",
|
||||
float64(1),
|
||||
},
|
||||
{
|
||||
"test_timing_stddev",
|
||||
float64(0),
|
||||
},
|
||||
{
|
||||
"test_timing_upper",
|
||||
float64(1),
|
||||
},
|
||||
{
|
||||
"test_timing_lower",
|
||||
float64(1),
|
||||
},
|
||||
{
|
||||
"test_timing_count",
|
||||
int64(5),
|
||||
},
|
||||
{
|
||||
"test_timing_percentile_90",
|
||||
float64(1),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
acc.AssertContainsFields(t, test.name,
|
||||
map[string]interface{}{"value": test.value})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_Timings_Delete(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
s.DeleteTimings = true
|
||||
fakeacc := &testutil.Accumulator{}
|
||||
var err error
|
||||
|
||||
line := "timing:100|ms"
|
||||
err = s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
|
||||
if len(s.timings) != 1 {
|
||||
t.Errorf("Should be 1 timing, found %d", len(s.timings))
|
||||
}
|
||||
|
||||
s.Gather(fakeacc)
|
||||
|
||||
if len(s.timings) != 0 {
|
||||
t.Errorf("All timings should have been deleted, found %d", len(s.timings))
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the delete_gauges option
|
||||
func TestParse_Gauges_Delete(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
s.DeleteGauges = true
|
||||
fakeacc := &testutil.Accumulator{}
|
||||
var err error
|
||||
|
||||
line := "current.users:100|g"
|
||||
err = s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
|
||||
err = test_validate_gauge("current_users", 100, s.gauges)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
s.Gather(fakeacc)
|
||||
|
||||
err = test_validate_gauge("current_users", 100, s.gauges)
|
||||
if err == nil {
|
||||
t.Error("current_users_gauge metric should have been deleted")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the delete_sets option
|
||||
func TestParse_Sets_Delete(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
s.DeleteSets = true
|
||||
fakeacc := &testutil.Accumulator{}
|
||||
var err error
|
||||
|
||||
line := "unique.user.ids:100|s"
|
||||
err = s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
|
||||
err = test_validate_set("unique_user_ids", 1, s.sets)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
s.Gather(fakeacc)
|
||||
|
||||
err = test_validate_set("unique_user_ids", 1, s.sets)
|
||||
if err == nil {
|
||||
t.Error("unique_user_ids_set metric should have been deleted")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the delete_counters option
|
||||
func TestParse_Counters_Delete(t *testing.T) {
|
||||
s := NewStatsd()
|
||||
s.DeleteCounters = true
|
||||
fakeacc := &testutil.Accumulator{}
|
||||
var err error
|
||||
|
||||
line := "total.users:100|c"
|
||||
err = s.parseStatsdLine(line)
|
||||
if err != nil {
|
||||
t.Errorf("Parsing line %s should not have resulted in an error\n", line)
|
||||
}
|
||||
|
||||
err = test_validate_counter("total_users", 100, s.counters)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
s.Gather(fakeacc)
|
||||
|
||||
err = test_validate_counter("total_users", 100, s.counters)
|
||||
if err == nil {
|
||||
t.Error("total_users_counter metric should have been deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKeyValue(t *testing.T) {
|
||||
k, v := parseKeyValue("foo=bar")
|
||||
if k != "foo" {
|
||||
t.Errorf("Expected %s, got %s", "foo", k)
|
||||
}
|
||||
if v != "bar" {
|
||||
t.Errorf("Expected %s, got %s", "bar", v)
|
||||
}
|
||||
|
||||
k2, v2 := parseKeyValue("baz")
|
||||
if k2 != "" {
|
||||
t.Errorf("Expected %s, got %s", "", k2)
|
||||
}
|
||||
if v2 != "baz" {
|
||||
t.Errorf("Expected %s, got %s", "baz", v2)
|
||||
}
|
||||
}
|
||||
|
||||
// Test utility functions
|
||||
|
||||
func test_validate_set(
|
||||
name string,
|
||||
value int64,
|
||||
cache map[string]cachedset,
|
||||
) error {
|
||||
var metric cachedset
|
||||
var found bool
|
||||
for _, v := range cache {
|
||||
if v.name == name {
|
||||
metric = v
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return errors.New(fmt.Sprintf("Test Error: Metric name %s not found\n", name))
|
||||
}
|
||||
|
||||
if value != int64(len(metric.set)) {
|
||||
return errors.New(fmt.Sprintf("Measurement: %s, expected %d, actual %d\n",
|
||||
name, value, len(metric.set)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func test_validate_counter(
|
||||
name string,
|
||||
value int64,
|
||||
cache map[string]cachedcounter,
|
||||
) error {
|
||||
var metric cachedcounter
|
||||
var found bool
|
||||
for _, v := range cache {
|
||||
if v.name == name {
|
||||
metric = v
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return errors.New(fmt.Sprintf("Test Error: Metric name %s not found\n", name))
|
||||
}
|
||||
|
||||
if value != metric.value {
|
||||
return errors.New(fmt.Sprintf("Measurement: %s, expected %d, actual %d\n",
|
||||
name, value, metric.value))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func test_validate_gauge(
|
||||
name string,
|
||||
value float64,
|
||||
cache map[string]cachedgauge,
|
||||
) error {
|
||||
var metric cachedgauge
|
||||
var found bool
|
||||
for _, v := range cache {
|
||||
if v.name == name {
|
||||
metric = v
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return errors.New(fmt.Sprintf("Test Error: Metric name %s not found\n", name))
|
||||
}
|
||||
|
||||
if value != metric.value {
|
||||
return errors.New(fmt.Sprintf("Measurement: %s, expected %f, actual %f\n",
|
||||
name, value, metric.value))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
83
plugins/inputs/system/CPU_README.md
Normal file
83
plugins/inputs/system/CPU_README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Telegraf plugin: CPU
|
||||
|
||||
#### Plugin arguments:
|
||||
- **totalcpu** boolean: If true, include `cpu-total` data
|
||||
- **percpu** boolean: If true, include data on a per-cpu basis `cpu0, cpu1, etc.`
|
||||
|
||||
#### Description
|
||||
|
||||
The CPU plugin collects standard CPU metrics as defined in `man proc`. All
|
||||
architectures do not support all of these metrics.
|
||||
|
||||
```
|
||||
cpu 3357 0 4313 1362393
|
||||
The amount of time, measured in units of USER_HZ (1/100ths of a second on
|
||||
most architectures, use sysconf(_SC_CLK_TCK) to obtain the right value),
|
||||
that the system spent in various states:
|
||||
|
||||
user (1) Time spent in user mode.
|
||||
|
||||
nice (2) Time spent in user mode with low priority (nice).
|
||||
|
||||
system (3) Time spent in system mode.
|
||||
|
||||
idle (4) Time spent in the idle task. This value should be USER_HZ times
|
||||
the second entry in the /proc/uptime pseudo-file.
|
||||
|
||||
iowait (since Linux 2.5.41)
|
||||
(5) Time waiting for I/O to complete.
|
||||
|
||||
irq (since Linux 2.6.0-test4)
|
||||
(6) Time servicing interrupts.
|
||||
|
||||
softirq (since Linux 2.6.0-test4)
|
||||
(7) Time servicing softirqs.
|
||||
|
||||
steal (since Linux 2.6.11)
|
||||
(8) Stolen time, which is the time spent in other operating systems
|
||||
when running in a virtualized environment
|
||||
|
||||
guest (since Linux 2.6.24)
|
||||
(9) Time spent running a virtual CPU for guest operating systems
|
||||
under the control of the Linux kernel.
|
||||
|
||||
guest_nice (since Linux 2.6.33)
|
||||
(10) Time spent running a niced guest (virtual CPU for guest operating systems under the control of the Linux kernel).
|
||||
```
|
||||
|
||||
# Measurements:
|
||||
### CPU Time measurements:
|
||||
|
||||
Meta:
|
||||
- units: CPU Time
|
||||
- tags: `cpu=<cpuN> or <cpu-total>`
|
||||
|
||||
Measurement names:
|
||||
- cpu_time_user
|
||||
- cpu_time_system
|
||||
- cpu_time_idle
|
||||
- cpu_time_nice
|
||||
- cpu_time_iowait
|
||||
- cpu_time_irq
|
||||
- cpu_time_softirq
|
||||
- cpu_time_steal
|
||||
- cpu_time_guest
|
||||
- cpu_time_guest_nice
|
||||
|
||||
### CPU Usage Percent Measurements:
|
||||
|
||||
Meta:
|
||||
- units: percent (out of 100)
|
||||
- tags: `cpu=<cpuN> or <cpu-total>`
|
||||
|
||||
Measurement names:
|
||||
- cpu_usage_user
|
||||
- cpu_usage_system
|
||||
- cpu_usage_idle
|
||||
- cpu_usage_nice
|
||||
- cpu_usage_iowait
|
||||
- cpu_usage_irq
|
||||
- cpu_usage_softirq
|
||||
- cpu_usage_steal
|
||||
- cpu_usage_guest
|
||||
- cpu_usage_guest_nice
|
||||
44
plugins/inputs/system/MEM_README.md
Normal file
44
plugins/inputs/system/MEM_README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
## Telegraf Plugin: MEM
|
||||
|
||||
#### Description
|
||||
|
||||
The mem plugin collects memory metrics, defined as follows. For a more complete
|
||||
explanation of the difference between `used` and `actual_used` RAM, see
|
||||
[Linux ate my ram](http://www.linuxatemyram.com/).
|
||||
|
||||
- **total**: total physical memory available
|
||||
- **available**: the actual amount of available memory that can be given instantly
|
||||
to processes that request more memory in bytes; In linux kernel 3.14+, this
|
||||
is available natively in /proc/meminfo. In other platforms, this is calculated by
|
||||
summing different memory values depending on the platform
|
||||
(e.g. free + buffers + cached on Linux).
|
||||
It is supposed to be used to monitor actual memory usage in a cross platform fashion.
|
||||
- **available_percent**: Percent of memory available, `available / total * 100`
|
||||
- **used**: memory used, calculated differently depending on the platform and
|
||||
designed for informational purposes only.
|
||||
- **free**: memory not being used at all (zeroed) that is readily available; note
|
||||
that this doesn't reflect the actual memory available (use 'available' instead).
|
||||
- **used_percent**: the percentage usage calculated as `(total - used) / total * 100`
|
||||
|
||||
## Measurements:
|
||||
#### Raw Memory measurements:
|
||||
|
||||
Meta:
|
||||
- units: bytes
|
||||
- tags: `nil`
|
||||
|
||||
Measurement names:
|
||||
- mem_total
|
||||
- mem_available
|
||||
- mem_used
|
||||
- mem_free
|
||||
|
||||
#### Derived usage percentages:
|
||||
|
||||
Meta:
|
||||
- units: percent (out of 100)
|
||||
- tags: `nil`
|
||||
|
||||
Measurement names:
|
||||
- mem_used_percent
|
||||
- mem_available_percent
|
||||
52
plugins/inputs/system/NETSTAT_README.md
Normal file
52
plugins/inputs/system/NETSTAT_README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
Telegraf plugin: NETSTAT
|
||||
|
||||
#### Description
|
||||
|
||||
The NETSTAT plugin collects TCP connections state and UDP socket counts by using `lsof`.
|
||||
|
||||
Supported TCP Connection states are follows.
|
||||
|
||||
- established
|
||||
- syn_sent
|
||||
- syn_recv
|
||||
- fin_wait1
|
||||
- fin_wait2
|
||||
- time_wait
|
||||
- close
|
||||
- close_wait
|
||||
- last_ack
|
||||
- listen
|
||||
- closing
|
||||
- none
|
||||
|
||||
|
||||
# Measurements:
|
||||
### TCP Connection State measurements:
|
||||
|
||||
Meta:
|
||||
- units: counts
|
||||
|
||||
Measurement names:
|
||||
- tcp_established
|
||||
- tcp_syn_sent
|
||||
- tcp_syn_recv
|
||||
- tcp_fin_wait1
|
||||
- tcp_fin_wait2
|
||||
- tcp_time_wait
|
||||
- tcp_close
|
||||
- tcp_close_wait
|
||||
- tcp_last_ack
|
||||
- tcp_listen
|
||||
- tcp_closing
|
||||
- tcp_none
|
||||
|
||||
If there are no connection on the state, the metric is not counted.
|
||||
|
||||
### UDP socket counts measurements:
|
||||
|
||||
Meta:
|
||||
- units: counts
|
||||
|
||||
Measurement names:
|
||||
- udp_socket
|
||||
|
||||
117
plugins/inputs/system/cpu.go
Normal file
117
plugins/inputs/system/cpu.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
)
|
||||
|
||||
type CPUStats struct {
|
||||
ps PS
|
||||
lastStats []cpu.CPUTimesStat
|
||||
|
||||
PerCPU bool `toml:"percpu"`
|
||||
TotalCPU bool `toml:"totalcpu"`
|
||||
}
|
||||
|
||||
func NewCPUStats(ps PS) *CPUStats {
|
||||
return &CPUStats{
|
||||
ps: ps,
|
||||
}
|
||||
}
|
||||
|
||||
func (_ *CPUStats) Description() string {
|
||||
return "Read metrics about cpu usage"
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
# Whether to report per-cpu stats or not
|
||||
percpu = true
|
||||
# Whether to report total system cpu stats or not
|
||||
totalcpu = true
|
||||
# Comment this line if you want the raw CPU time metrics
|
||||
drop = ["time_*"]
|
||||
`
|
||||
|
||||
func (_ *CPUStats) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (s *CPUStats) Gather(acc inputs.Accumulator) error {
|
||||
times, err := s.ps.CPUTimes(s.PerCPU, s.TotalCPU)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting CPU info: %s", err)
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
for i, cts := range times {
|
||||
tags := map[string]string{
|
||||
"cpu": cts.CPU,
|
||||
}
|
||||
|
||||
total := totalCpuTime(cts)
|
||||
|
||||
// Add cpu time metrics
|
||||
fields := map[string]interface{}{
|
||||
"time_user": cts.User,
|
||||
"time_system": cts.System,
|
||||
"time_idle": cts.Idle,
|
||||
"time_nice": cts.Nice,
|
||||
"time_iowait": cts.Iowait,
|
||||
"time_irq": cts.Irq,
|
||||
"time_softirq": cts.Softirq,
|
||||
"time_steal": cts.Steal,
|
||||
"time_guest": cts.Guest,
|
||||
"time_guest_nice": cts.GuestNice,
|
||||
}
|
||||
|
||||
// Add in percentage
|
||||
if len(s.lastStats) == 0 {
|
||||
acc.AddFields("cpu", fields, tags, now)
|
||||
// If it's the 1st gather, can't get CPU Usage stats yet
|
||||
continue
|
||||
}
|
||||
lastCts := s.lastStats[i]
|
||||
lastTotal := totalCpuTime(lastCts)
|
||||
totalDelta := total - lastTotal
|
||||
|
||||
if totalDelta < 0 {
|
||||
s.lastStats = times
|
||||
return fmt.Errorf("Error: current total CPU time is less than previous total CPU time")
|
||||
}
|
||||
|
||||
if totalDelta == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields["usage_user"] = 100 * (cts.User - lastCts.User) / totalDelta
|
||||
fields["usage_system"] = 100 * (cts.System - lastCts.System) / totalDelta
|
||||
fields["usage_idle"] = 100 * (cts.Idle - lastCts.Idle) / totalDelta
|
||||
fields["usage_nice"] = 100 * (cts.Nice - lastCts.Nice) / totalDelta
|
||||
fields["usage_iowait"] = 100 * (cts.Iowait - lastCts.Iowait) / totalDelta
|
||||
fields["usage_irq"] = 100 * (cts.Irq - lastCts.Irq) / totalDelta
|
||||
fields["usage_softirq"] = 100 * (cts.Softirq - lastCts.Softirq) / totalDelta
|
||||
fields["usage_steal"] = 100 * (cts.Steal - lastCts.Steal) / totalDelta
|
||||
fields["usage_guest"] = 100 * (cts.Guest - lastCts.Guest) / totalDelta
|
||||
fields["usage_guest_nice"] = 100 * (cts.GuestNice - lastCts.GuestNice) / totalDelta
|
||||
acc.AddFields("cpu", fields, tags, now)
|
||||
}
|
||||
|
||||
s.lastStats = times
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func totalCpuTime(t cpu.CPUTimesStat) float64 {
|
||||
total := t.User + t.System + t.Nice + t.Iowait + t.Irq + t.Softirq + t.Steal +
|
||||
t.Guest + t.GuestNice + t.Idle
|
||||
return total
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("cpu", func() inputs.Input {
|
||||
return &CPUStats{ps: &systemPS{}}
|
||||
})
|
||||
}
|
||||
148
plugins/inputs/system/cpu_test.go
Normal file
148
plugins/inputs/system/cpu_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCPUStats(t *testing.T) {
|
||||
var mps MockPS
|
||||
defer mps.AssertExpectations(t)
|
||||
var acc testutil.Accumulator
|
||||
|
||||
cts := cpu.CPUTimesStat{
|
||||
CPU: "cpu0",
|
||||
User: 3.1,
|
||||
System: 8.2,
|
||||
Idle: 80.1,
|
||||
Nice: 1.3,
|
||||
Iowait: 0.2,
|
||||
Irq: 0.1,
|
||||
Softirq: 0.11,
|
||||
Steal: 0.0511,
|
||||
Guest: 8.1,
|
||||
GuestNice: 0.324,
|
||||
}
|
||||
|
||||
cts2 := cpu.CPUTimesStat{
|
||||
CPU: "cpu0",
|
||||
User: 11.4, // increased by 8.3
|
||||
System: 10.9, // increased by 2.7
|
||||
Idle: 158.8699, // increased by 78.7699 (for total increase of 100)
|
||||
Nice: 2.5, // increased by 1.2
|
||||
Iowait: 0.7, // increased by 0.5
|
||||
Irq: 1.2, // increased by 1.1
|
||||
Softirq: 0.31, // increased by 0.2
|
||||
Steal: 0.2812, // increased by 0.0001
|
||||
Guest: 12.9, // increased by 4.8
|
||||
GuestNice: 2.524, // increased by 2.2
|
||||
}
|
||||
|
||||
mps.On("CPUTimes").Return([]cpu.CPUTimesStat{cts}, nil)
|
||||
|
||||
cs := NewCPUStats(&mps)
|
||||
|
||||
cputags := map[string]string{
|
||||
"cpu": "cpu0",
|
||||
}
|
||||
|
||||
err := cs.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Computed values are checked with delta > 0 becasue of floating point arithmatic
|
||||
// imprecision
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_user", 3.1, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_system", 8.2, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_idle", 80.1, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_nice", 1.3, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_iowait", 0.2, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_irq", 0.1, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_softirq", 0.11, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_steal", 0.0511, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_guest", 8.1, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_guest_nice", 0.324, 0, cputags)
|
||||
|
||||
mps2 := MockPS{}
|
||||
mps2.On("CPUTimes").Return([]cpu.CPUTimesStat{cts2}, nil)
|
||||
cs.ps = &mps2
|
||||
|
||||
// Should have added cpu percentages too
|
||||
err = cs.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_user", 11.4, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_system", 10.9, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_idle", 158.8699, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_nice", 2.5, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_iowait", 0.7, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_irq", 1.2, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_softirq", 0.31, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_steal", 0.2812, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_guest", 12.9, 0, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "time_guest_nice", 2.524, 0, cputags)
|
||||
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_user", 8.3, 0.0005, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_system", 2.7, 0.0005, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_idle", 78.7699, 0.0005, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_nice", 1.2, 0.0005, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_iowait", 0.5, 0.0005, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_irq", 1.1, 0.0005, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_softirq", 0.2, 0.0005, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_steal", 0.2301, 0.0005, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_guest", 4.8, 0.0005, cputags)
|
||||
assertContainsTaggedFloat(t, &acc, "cpu", "usage_guest_nice", 2.2, 0.0005, cputags)
|
||||
}
|
||||
|
||||
// Asserts that a given accumulator contains a measurment of type float64 with
|
||||
// specific tags within a certain distance of a given expected value. Asserts a failure
|
||||
// if the measurement is of the wrong type, or if no matching measurements are found
|
||||
//
|
||||
// Paramaters:
|
||||
// t *testing.T : Testing object to use
|
||||
// acc testutil.Accumulator: Accumulator to examine
|
||||
// measurement string : Name of the measurement to examine
|
||||
// expectedValue float64 : Value to search for within the measurement
|
||||
// delta float64 : Maximum acceptable distance of an accumulated value
|
||||
// from the expectedValue parameter. Useful when
|
||||
// floating-point arithmatic imprecision makes looking
|
||||
// for an exact match impractical
|
||||
// tags map[string]string : Tag set the found measurement must have. Set to nil to
|
||||
// ignore the tag set.
|
||||
func assertContainsTaggedFloat(
|
||||
t *testing.T,
|
||||
acc *testutil.Accumulator,
|
||||
measurement string,
|
||||
field string,
|
||||
expectedValue float64,
|
||||
delta float64,
|
||||
tags map[string]string,
|
||||
) {
|
||||
var actualValue float64
|
||||
for _, pt := range acc.Points {
|
||||
if pt.Measurement == measurement {
|
||||
for fieldname, value := range pt.Fields {
|
||||
if fieldname == field {
|
||||
if value, ok := value.(float64); ok {
|
||||
actualValue = value
|
||||
if (value >= expectedValue-delta) && (value <= expectedValue+delta) {
|
||||
// Found the point, return without failing
|
||||
return
|
||||
}
|
||||
} else {
|
||||
assert.Fail(t, fmt.Sprintf("Measurement \"%s\" does not have type float64",
|
||||
measurement))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
msg := fmt.Sprintf(
|
||||
"Could not find measurement \"%s\" with requested tags within %f of %f, Actual: %f",
|
||||
measurement, delta, expectedValue, actualValue)
|
||||
assert.Fail(t, msg)
|
||||
}
|
||||
144
plugins/inputs/system/disk.go
Normal file
144
plugins/inputs/system/disk.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type DiskStats struct {
|
||||
ps PS
|
||||
|
||||
Mountpoints []string
|
||||
}
|
||||
|
||||
func (_ *DiskStats) Description() string {
|
||||
return "Read metrics about disk usage by mount point"
|
||||
}
|
||||
|
||||
var diskSampleConfig = `
|
||||
# By default, telegraf gather stats for all mountpoints.
|
||||
# Setting mountpoints will restrict the stats to the specified mountpoints.
|
||||
# Mountpoints=["/"]
|
||||
`
|
||||
|
||||
func (_ *DiskStats) SampleConfig() string {
|
||||
return diskSampleConfig
|
||||
}
|
||||
|
||||
func (s *DiskStats) Gather(acc inputs.Accumulator) error {
|
||||
disks, err := s.ps.DiskUsage()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting disk usage info: %s", err)
|
||||
}
|
||||
|
||||
var restrictMpoints bool
|
||||
mPoints := make(map[string]bool)
|
||||
if len(s.Mountpoints) != 0 {
|
||||
restrictMpoints = true
|
||||
for _, mp := range s.Mountpoints {
|
||||
mPoints[mp] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, du := range disks {
|
||||
_, member := mPoints[du.Path]
|
||||
if restrictMpoints && !member {
|
||||
continue
|
||||
}
|
||||
tags := map[string]string{
|
||||
"path": du.Path,
|
||||
"fstype": du.Fstype,
|
||||
}
|
||||
fields := map[string]interface{}{
|
||||
"total": du.Total,
|
||||
"free": du.Free,
|
||||
"used": du.Total - du.Free,
|
||||
"inodes_total": du.InodesTotal,
|
||||
"inodes_free": du.InodesFree,
|
||||
"inodes_used": du.InodesTotal - du.InodesFree,
|
||||
}
|
||||
acc.AddFields("disk", fields, tags)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DiskIOStats struct {
|
||||
ps PS
|
||||
|
||||
Devices []string
|
||||
SkipSerialNumber bool
|
||||
}
|
||||
|
||||
func (_ *DiskIOStats) Description() string {
|
||||
return "Read metrics about disk IO by device"
|
||||
}
|
||||
|
||||
var diskIoSampleConfig = `
|
||||
# By default, telegraf will gather stats for all devices including
|
||||
# disk partitions.
|
||||
# Setting devices will restrict the stats to the specified devcies.
|
||||
# devices = ["sda","sdb"]
|
||||
# Uncomment the following line if you do not need disk serial numbers.
|
||||
# skip_serial_number = true
|
||||
`
|
||||
|
||||
func (_ *DiskIOStats) SampleConfig() string {
|
||||
return diskIoSampleConfig
|
||||
}
|
||||
|
||||
func (s *DiskIOStats) Gather(acc inputs.Accumulator) error {
|
||||
diskio, err := s.ps.DiskIO()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting disk io info: %s", err)
|
||||
}
|
||||
|
||||
var restrictDevices bool
|
||||
devices := make(map[string]bool)
|
||||
if len(s.Devices) != 0 {
|
||||
restrictDevices = true
|
||||
for _, dev := range s.Devices {
|
||||
devices[dev] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, io := range diskio {
|
||||
_, member := devices[io.Name]
|
||||
if restrictDevices && !member {
|
||||
continue
|
||||
}
|
||||
tags := map[string]string{}
|
||||
tags["name"] = io.Name
|
||||
if !s.SkipSerialNumber {
|
||||
if len(io.SerialNumber) != 0 {
|
||||
tags["serial"] = io.SerialNumber
|
||||
} else {
|
||||
tags["serial"] = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"reads": io.ReadCount,
|
||||
"writes": io.WriteCount,
|
||||
"read_bytes": io.ReadBytes,
|
||||
"write_bytes": io.WriteBytes,
|
||||
"read_time": io.ReadTime,
|
||||
"write_time": io.WriteTime,
|
||||
"io_time": io.IoTime,
|
||||
}
|
||||
acc.AddFields("diskio", fields, tags)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("disk", func() inputs.Input {
|
||||
return &DiskStats{ps: &systemPS{}}
|
||||
})
|
||||
|
||||
inputs.Add("diskio", func() inputs.Input {
|
||||
return &DiskIOStats{ps: &systemPS{}}
|
||||
})
|
||||
}
|
||||
165
plugins/inputs/system/disk_test.go
Normal file
165
plugins/inputs/system/disk_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDiskStats(t *testing.T) {
|
||||
var mps MockPS
|
||||
defer mps.AssertExpectations(t)
|
||||
var acc testutil.Accumulator
|
||||
var err error
|
||||
|
||||
du := []*disk.DiskUsageStat{
|
||||
{
|
||||
Path: "/",
|
||||
Fstype: "ext4",
|
||||
Total: 128,
|
||||
Free: 23,
|
||||
InodesTotal: 1234,
|
||||
InodesFree: 234,
|
||||
},
|
||||
{
|
||||
Path: "/home",
|
||||
Fstype: "ext4",
|
||||
Total: 256,
|
||||
Free: 46,
|
||||
InodesTotal: 2468,
|
||||
InodesFree: 468,
|
||||
},
|
||||
}
|
||||
|
||||
mps.On("DiskUsage").Return(du, nil)
|
||||
|
||||
err = (&DiskStats{ps: &mps}).Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
numDiskPoints := acc.NFields()
|
||||
expectedAllDiskPoints := 12
|
||||
assert.Equal(t, expectedAllDiskPoints, numDiskPoints)
|
||||
|
||||
tags1 := map[string]string{
|
||||
"path": "/",
|
||||
"fstype": "ext4",
|
||||
}
|
||||
tags2 := map[string]string{
|
||||
"path": "/home",
|
||||
"fstype": "ext4",
|
||||
}
|
||||
|
||||
fields1 := map[string]interface{}{
|
||||
"total": uint64(128), //tags1)
|
||||
"used": uint64(105), //tags1)
|
||||
"free": uint64(23), //tags1)
|
||||
"inodes_total": uint64(1234), //tags1)
|
||||
"inodes_free": uint64(234), //tags1)
|
||||
"inodes_used": uint64(1000), //tags1)
|
||||
}
|
||||
fields2 := map[string]interface{}{
|
||||
"total": uint64(256), //tags2)
|
||||
"used": uint64(210), //tags2)
|
||||
"free": uint64(46), //tags2)
|
||||
"inodes_total": uint64(2468), //tags2)
|
||||
"inodes_free": uint64(468), //tags2)
|
||||
"inodes_used": uint64(2000), //tags2)
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "disk", fields1, tags1)
|
||||
acc.AssertContainsTaggedFields(t, "disk", fields2, tags2)
|
||||
|
||||
// We expect 6 more DiskPoints to show up with an explicit match on "/"
|
||||
// and /home not matching the /dev in Mountpoints
|
||||
err = (&DiskStats{ps: &mps, Mountpoints: []string{"/", "/dev"}}).Gather(&acc)
|
||||
assert.Equal(t, expectedAllDiskPoints+6, acc.NFields())
|
||||
|
||||
// We should see all the diskpoints as Mountpoints includes both
|
||||
// / and /home
|
||||
err = (&DiskStats{ps: &mps, Mountpoints: []string{"/", "/home"}}).Gather(&acc)
|
||||
assert.Equal(t, 2*expectedAllDiskPoints+6, acc.NFields())
|
||||
}
|
||||
|
||||
// func TestDiskIOStats(t *testing.T) {
|
||||
// var mps MockPS
|
||||
// defer mps.AssertExpectations(t)
|
||||
// var acc testutil.Accumulator
|
||||
// var err error
|
||||
|
||||
// diskio1 := disk.DiskIOCountersStat{
|
||||
// ReadCount: 888,
|
||||
// WriteCount: 5341,
|
||||
// ReadBytes: 100000,
|
||||
// WriteBytes: 200000,
|
||||
// ReadTime: 7123,
|
||||
// WriteTime: 9087,
|
||||
// Name: "sda1",
|
||||
// IoTime: 123552,
|
||||
// SerialNumber: "ab-123-ad",
|
||||
// }
|
||||
// diskio2 := disk.DiskIOCountersStat{
|
||||
// ReadCount: 444,
|
||||
// WriteCount: 2341,
|
||||
// ReadBytes: 200000,
|
||||
// WriteBytes: 400000,
|
||||
// ReadTime: 3123,
|
||||
// WriteTime: 6087,
|
||||
// Name: "sdb1",
|
||||
// IoTime: 246552,
|
||||
// SerialNumber: "bb-123-ad",
|
||||
// }
|
||||
|
||||
// mps.On("DiskIO").Return(
|
||||
// map[string]disk.DiskIOCountersStat{"sda1": diskio1, "sdb1": diskio2},
|
||||
// nil)
|
||||
|
||||
// err = (&DiskIOStats{ps: &mps}).Gather(&acc)
|
||||
// require.NoError(t, err)
|
||||
|
||||
// numDiskIOPoints := acc.NFields()
|
||||
// expectedAllDiskIOPoints := 14
|
||||
// assert.Equal(t, expectedAllDiskIOPoints, numDiskIOPoints)
|
||||
|
||||
// dtags1 := map[string]string{
|
||||
// "name": "sda1",
|
||||
// "serial": "ab-123-ad",
|
||||
// }
|
||||
// dtags2 := map[string]string{
|
||||
// "name": "sdb1",
|
||||
// "serial": "bb-123-ad",
|
||||
// }
|
||||
|
||||
// assert.True(t, acc.CheckTaggedValue("reads", uint64(888), dtags1))
|
||||
// assert.True(t, acc.CheckTaggedValue("writes", uint64(5341), dtags1))
|
||||
// assert.True(t, acc.CheckTaggedValue("read_bytes", uint64(100000), dtags1))
|
||||
// assert.True(t, acc.CheckTaggedValue("write_bytes", uint64(200000), dtags1))
|
||||
// assert.True(t, acc.CheckTaggedValue("read_time", uint64(7123), dtags1))
|
||||
// assert.True(t, acc.CheckTaggedValue("write_time", uint64(9087), dtags1))
|
||||
// assert.True(t, acc.CheckTaggedValue("io_time", uint64(123552), dtags1))
|
||||
// assert.True(t, acc.CheckTaggedValue("reads", uint64(444), dtags2))
|
||||
// assert.True(t, acc.CheckTaggedValue("writes", uint64(2341), dtags2))
|
||||
// assert.True(t, acc.CheckTaggedValue("read_bytes", uint64(200000), dtags2))
|
||||
// assert.True(t, acc.CheckTaggedValue("write_bytes", uint64(400000), dtags2))
|
||||
// assert.True(t, acc.CheckTaggedValue("read_time", uint64(3123), dtags2))
|
||||
// assert.True(t, acc.CheckTaggedValue("write_time", uint64(6087), dtags2))
|
||||
// assert.True(t, acc.CheckTaggedValue("io_time", uint64(246552), dtags2))
|
||||
|
||||
// // We expect 7 more DiskIOPoints to show up with an explicit match on "sdb1"
|
||||
// // and serial should be missing from the tags with SkipSerialNumber set
|
||||
// err = (&DiskIOStats{ps: &mps, Devices: []string{"sdb1"}, SkipSerialNumber: true}).Gather(&acc)
|
||||
// assert.Equal(t, expectedAllDiskIOPoints+7, acc.NFields())
|
||||
|
||||
// dtags3 := map[string]string{
|
||||
// "name": "sdb1",
|
||||
// }
|
||||
|
||||
// assert.True(t, acc.CheckTaggedValue("reads", uint64(444), dtags3))
|
||||
// assert.True(t, acc.CheckTaggedValue("writes", uint64(2341), dtags3))
|
||||
// assert.True(t, acc.CheckTaggedValue("read_bytes", uint64(200000), dtags3))
|
||||
// assert.True(t, acc.CheckTaggedValue("write_bytes", uint64(400000), dtags3))
|
||||
// assert.True(t, acc.CheckTaggedValue("read_time", uint64(3123), dtags3))
|
||||
// assert.True(t, acc.CheckTaggedValue("write_time", uint64(6087), dtags3))
|
||||
// assert.True(t, acc.CheckTaggedValue("io_time", uint64(246552), dtags3))
|
||||
// }
|
||||
89
plugins/inputs/system/docker.go
Normal file
89
plugins/inputs/system/docker.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// +build linux
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/influxdb/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
type DockerStats struct {
|
||||
ps PS
|
||||
}
|
||||
|
||||
func (_ *DockerStats) Description() string {
|
||||
return "Read metrics about docker containers"
|
||||
}
|
||||
|
||||
func (_ *DockerStats) SampleConfig() string { return "" }
|
||||
|
||||
func (s *DockerStats) Gather(acc inputs.Accumulator) error {
|
||||
containers, err := s.ps.DockerStat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting docker info: %s", err)
|
||||
}
|
||||
|
||||
for _, cont := range containers {
|
||||
tags := map[string]string{
|
||||
"id": cont.Id,
|
||||
"name": cont.Name,
|
||||
"command": cont.Command,
|
||||
}
|
||||
for k, v := range cont.Labels {
|
||||
tags[k] = v
|
||||
}
|
||||
|
||||
cts := cont.CPU
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"user": cts.User,
|
||||
"system": cts.System,
|
||||
"idle": cts.Idle,
|
||||
"nice": cts.Nice,
|
||||
"iowait": cts.Iowait,
|
||||
"irq": cts.Irq,
|
||||
"softirq": cts.Softirq,
|
||||
"steal": cts.Steal,
|
||||
"guest": cts.Guest,
|
||||
"guest_nice": cts.GuestNice,
|
||||
|
||||
"cache": cont.Mem.Cache,
|
||||
"rss": cont.Mem.RSS,
|
||||
"rss_huge": cont.Mem.RSSHuge,
|
||||
"mapped_file": cont.Mem.MappedFile,
|
||||
"swap_in": cont.Mem.Pgpgin,
|
||||
"swap_out": cont.Mem.Pgpgout,
|
||||
"page_fault": cont.Mem.Pgfault,
|
||||
"page_major_fault": cont.Mem.Pgmajfault,
|
||||
"inactive_anon": cont.Mem.InactiveAnon,
|
||||
"active_anon": cont.Mem.ActiveAnon,
|
||||
"inactive_file": cont.Mem.InactiveFile,
|
||||
"active_file": cont.Mem.ActiveFile,
|
||||
"unevictable": cont.Mem.Unevictable,
|
||||
"memory_limit": cont.Mem.HierarchicalMemoryLimit,
|
||||
"total_cache": cont.Mem.TotalCache,
|
||||
"total_rss": cont.Mem.TotalRSS,
|
||||
"total_rss_huge": cont.Mem.TotalRSSHuge,
|
||||
"total_mapped_file": cont.Mem.TotalMappedFile,
|
||||
"total_swap_in": cont.Mem.TotalPgpgIn,
|
||||
"total_swap_out": cont.Mem.TotalPgpgOut,
|
||||
"total_page_fault": cont.Mem.TotalPgFault,
|
||||
"total_page_major_fault": cont.Mem.TotalPgMajFault,
|
||||
"total_inactive_anon": cont.Mem.TotalInactiveAnon,
|
||||
"total_active_anon": cont.Mem.TotalActiveAnon,
|
||||
"total_inactive_file": cont.Mem.TotalInactiveFile,
|
||||
"total_active_file": cont.Mem.TotalActiveFile,
|
||||
"total_unevictable": cont.Mem.TotalUnevictable,
|
||||
}
|
||||
acc.AddFields("docker", fields, tags)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("docker", func() inputs.Input {
|
||||
return &DockerStats{ps: &systemPS{}}
|
||||
})
|
||||
}
|
||||
119
plugins/inputs/system/docker_test.go
Normal file
119
plugins/inputs/system/docker_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// +build linux
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdb/telegraf/testutil"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/docker"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDockerStats_GenerateStats(t *testing.T) {
|
||||
var mps MockPS
|
||||
var acc testutil.Accumulator
|
||||
|
||||
ds := &DockerContainerStat{
|
||||
Name: "blah",
|
||||
CPU: &cpu.CPUTimesStat{
|
||||
CPU: "all",
|
||||
User: 3.1,
|
||||
System: 8.2,
|
||||
Idle: 80.1,
|
||||
Nice: 1.3,
|
||||
Iowait: 0.2,
|
||||
Irq: 0.1,
|
||||
Softirq: 0.11,
|
||||
Steal: 0.0001,
|
||||
Guest: 8.1,
|
||||
GuestNice: 0.324,
|
||||
},
|
||||
Mem: &docker.CgroupMemStat{
|
||||
ContainerID: "blah",
|
||||
Cache: 1,
|
||||
RSS: 2,
|
||||
RSSHuge: 3,
|
||||
MappedFile: 4,
|
||||
Pgpgin: 5,
|
||||
Pgpgout: 6,
|
||||
Pgfault: 7,
|
||||
Pgmajfault: 8,
|
||||
InactiveAnon: 9,
|
||||
ActiveAnon: 10,
|
||||
InactiveFile: 11,
|
||||
ActiveFile: 12,
|
||||
Unevictable: 13,
|
||||
HierarchicalMemoryLimit: 14,
|
||||
TotalCache: 15,
|
||||
TotalRSS: 16,
|
||||
TotalRSSHuge: 17,
|
||||
TotalMappedFile: 18,
|
||||
TotalPgpgIn: 19,
|
||||
TotalPgpgOut: 20,
|
||||
TotalPgFault: 21,
|
||||
TotalPgMajFault: 22,
|
||||
TotalInactiveAnon: 23,
|
||||
TotalActiveAnon: 24,
|
||||
TotalInactiveFile: 25,
|
||||
TotalActiveFile: 26,
|
||||
TotalUnevictable: 27,
|
||||
},
|
||||
}
|
||||
|
||||
mps.On("DockerStat").Return([]*DockerContainerStat{ds}, nil)
|
||||
|
||||
err := (&DockerStats{&mps}).Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
dockertags := map[string]string{
|
||||
"name": "blah",
|
||||
"id": "",
|
||||
"command": "",
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"user": 3.1,
|
||||
"system": 8.2,
|
||||
"idle": 80.1,
|
||||
"nice": 1.3,
|
||||
"iowait": 0.2,
|
||||
"irq": 0.1,
|
||||
"softirq": 0.11,
|
||||
"steal": 0.0001,
|
||||
"guest": 8.1,
|
||||
"guest_nice": 0.324,
|
||||
|
||||
"cache": uint64(1),
|
||||
"rss": uint64(2),
|
||||
"rss_huge": uint64(3),
|
||||
"mapped_file": uint64(4),
|
||||
"swap_in": uint64(5),
|
||||
"swap_out": uint64(6),
|
||||
"page_fault": uint64(7),
|
||||
"page_major_fault": uint64(8),
|
||||
"inactive_anon": uint64(9),
|
||||
"active_anon": uint64(10),
|
||||
"inactive_file": uint64(11),
|
||||
"active_file": uint64(12),
|
||||
"unevictable": uint64(13),
|
||||
"memory_limit": uint64(14),
|
||||
"total_cache": uint64(15),
|
||||
"total_rss": uint64(16),
|
||||
"total_rss_huge": uint64(17),
|
||||
"total_mapped_file": uint64(18),
|
||||
"total_swap_in": uint64(19),
|
||||
"total_swap_out": uint64(20),
|
||||
"total_page_fault": uint64(21),
|
||||
"total_page_major_fault": uint64(22),
|
||||
"total_inactive_anon": uint64(23),
|
||||
"total_active_anon": uint64(24),
|
||||
"total_inactive_file": uint64(25),
|
||||
"total_active_file": uint64(26),
|
||||
"total_unevictable": uint64(27),
|
||||
}
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "docker", fields, dockertags)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user