Compare commits
56 Commits
1.2.0-rc3
...
cam-extern
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15e58c59fb | ||
|
|
694955c87b | ||
|
|
b1945c0493 | ||
|
|
1c4673e900 | ||
|
|
dfb4038654 | ||
|
|
b3537ef2a8 | ||
|
|
0ce44648cf | ||
|
|
55d3f70771 | ||
|
|
a610f8bd03 | ||
|
|
dfba3ff37a | ||
|
|
285be648c4 | ||
|
|
f7d551a807 | ||
|
|
3f224a15d5 | ||
|
|
c0bbde03ea | ||
|
|
97050e9669 | ||
|
|
eafd1dcc7c | ||
|
|
c528c53e5b | ||
|
|
07a6223932 | ||
|
|
aeb849d744 | ||
|
|
9003efc3fa | ||
|
|
32e06a489d | ||
|
|
2932db8480 | ||
|
|
19dee32287 | ||
|
|
4dad723088 | ||
|
|
54cfbb5b87 | ||
|
|
3e37dda7b0 | ||
|
|
fb7931591d | ||
|
|
e87ce22af9 | ||
|
|
738cbbdbb6 | ||
|
|
074e6d177c | ||
|
|
1d864ebd40 | ||
|
|
e9decadf75 | ||
|
|
3fa37a9212 | ||
|
|
c9e87a39f8 | ||
|
|
4a5d313693 | ||
|
|
168270ea5f | ||
|
|
c4d4185fb5 | ||
|
|
822333690f | ||
|
|
d7a8bb2214 | ||
|
|
a505123e60 | ||
|
|
be10b19760 | ||
|
|
b9ae3d6a57 | ||
|
|
c882570983 | ||
|
|
80411f99f0 | ||
|
|
6df3f0fdae | ||
|
|
22340ad984 | ||
|
|
c15504c509 | ||
|
|
20bf90ee52 | ||
|
|
3de6bfbcb8 | ||
|
|
e0c6262e0b | ||
|
|
9b2f6499e7 | ||
|
|
9262712f0a | ||
|
|
0c9da0985a | ||
|
|
b89c45b858 | ||
|
|
b60b360f13 | ||
|
|
734988d732 |
75
CHANGELOG.md
75
CHANGELOG.md
@@ -1,4 +1,71 @@
|
||||
## v1.2 [unreleased]
|
||||
## v1.3 [unreleased]
|
||||
|
||||
### Release Notes
|
||||
|
||||
- Ceph: the `ceph_pgmap_state` metric content has been modified to use a unique field `count`, with each state expressed as a `state` tag.
|
||||
|
||||
Telegraf < 1.3:
|
||||
|
||||
```
|
||||
# field_name value
|
||||
active+clean 123
|
||||
active+clean+scrubbing 3
|
||||
```
|
||||
|
||||
Telegraf >= 1.3:
|
||||
|
||||
```
|
||||
# field_name value tag
|
||||
count 123 state=active+clean
|
||||
count 3 state=active+clean+scrubbing
|
||||
```
|
||||
|
||||
- The [Riemann output plugin](./plugins/outputs/riemann) has been rewritten
|
||||
and the previous riemann plugin is _incompatible_ with the new one. The reasons
|
||||
for this are outlined in issue [#1878](https://github.com/influxdata/telegraf/issues/1878).
|
||||
The previous riemann output will still be available using
|
||||
`outputs.riemann_legacy` if needed, but that will eventually be deprecated.
|
||||
It is highly recommended that all users migrate to the new riemann output plugin.
|
||||
|
||||
### Features
|
||||
|
||||
- [#2204](https://github.com/influxdata/telegraf/pull/2204): Extend http_response to support searching for a substring in response. Return 1 if found, else 0.
|
||||
- [#2137](https://github.com/influxdata/telegraf/pull/2137): Added userstats to mysql input plugin.
|
||||
- [#2179](https://github.com/influxdata/telegraf/pull/2179): Added more InnoDB metric to MySQL plugin.
|
||||
- [#2229](https://github.com/influxdata/telegraf/pull/2229): `ceph_pgmap_state` metric now uses a single field `count`, with PG state published as `state` tag.
|
||||
- [#2251](https://github.com/influxdata/telegraf/pull/2251): InfluxDB output: use own client for improved through-put and less allocations.
|
||||
- [#2330](https://github.com/influxdata/telegraf/pull/2330): Keep -config-directory when running as Windows service.
|
||||
- [#1900](https://github.com/influxdata/telegraf/pull/1900): Riemann plugin rewrite.
|
||||
- [#1453](https://github.com/influxdata/telegraf/pull/1453): diskio: add support for name templates and udev tags.
|
||||
- [#2277](https://github.com/influxdata/telegraf/pull/2277): add integer metrics for Consul check health state.
|
||||
- [#2201](https://github.com/influxdata/telegraf/pull/2201): Add lock option to the IPtables input plugin.
|
||||
- [#2244](https://github.com/influxdata/telegraf/pull/2244): Support ipmi_sensor plugin querying local ipmi sensors.
|
||||
- [#2339](https://github.com/influxdata/telegraf/pull/2339): Increment gather_errors for all errors emitted by inputs.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- [#2077](https://github.com/influxdata/telegraf/issues/2077): SQL Server Input - Arithmetic overflow error converting numeric to data type int.
|
||||
- [#2262](https://github.com/influxdata/telegraf/issues/2262): Flush jitter can inhibit metric collection.
|
||||
- [#2287](https://github.com/influxdata/telegraf/issues/2287): Kubernetes input: Handle null startTime for stopped pods
|
||||
- [#1636](https://github.com/influxdata/telegraf/issues/1636): procstat - stop caching PIDs.
|
||||
- [#2318](https://github.com/influxdata/telegraf/issues/2318): haproxy input - Add missing fields.
|
||||
- [#2287](https://github.com/influxdata/telegraf/issues/2287): Kubernetes input: Handle null startTime for stopped pods.
|
||||
- [#2356](https://github.com/influxdata/telegraf/issues/2356): cpu input panic when /proc/stat is empty.
|
||||
- [#2341](https://github.com/influxdata/telegraf/issues/2341): telegraf swallowing panics in --test mode.
|
||||
- [#2358](https://github.com/influxdata/telegraf/pull/2358): Create pidfile with 644 permissions & defer file deletion.
|
||||
|
||||
## v1.2.1 [2017-02-01]
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- [#2317](https://github.com/influxdata/telegraf/issues/2317): Fix segfault on nil metrics with influxdb output.
|
||||
- [#2324](https://github.com/influxdata/telegraf/issues/2324): Fix negative number handling.
|
||||
|
||||
### Features
|
||||
|
||||
- [#2348](https://github.com/influxdata/telegraf/pull/2348): Go version 1.7.4 -> 1.7.5
|
||||
|
||||
## v1.2 [2017-01-00]
|
||||
|
||||
### Release Notes
|
||||
|
||||
@@ -45,6 +112,7 @@ plugins, not just statsd.
|
||||
- [#1980](https://github.com/influxdata/telegraf/issues/1980): Hide username/password from elasticsearch error log messages.
|
||||
- [#2097](https://github.com/influxdata/telegraf/issues/2097): Configurable HTTP timeouts in Jolokia plugin
|
||||
- [#2255](https://github.com/influxdata/telegraf/pull/2255): Allow changing jolokia attribute delimiter
|
||||
- [#2094](https://github.com/influxdata/telegraf/pull/2094): Add generic socket listener & writer.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
@@ -54,7 +122,7 @@ plugins, not just statsd.
|
||||
- [#1775](https://github.com/influxdata/telegraf/issues/1775): Cache & expire metrics for delivery to prometheus.
|
||||
- [#2146](https://github.com/influxdata/telegraf/issues/2146): Fix potential panic in aggregator plugin metric maker.
|
||||
- [#1843](https://github.com/influxdata/telegraf/pull/1843) & [#1668](https://github.com/influxdata/telegraf/issues/1668): Add optional ability to define PID as a tag.
|
||||
- [#1730](https://github.com/influxdata/telegraf/issues/1730): Fix win_perf_counters not gathering non-English counters.
|
||||
- [#1730](https://github.com/influxdata/telegraf/issues/1730) & [#2261](https://github.com/influxdata/telegraf/pull/2261): Fix win_perf_counters not gathering non-English counters.
|
||||
- [#2061](https://github.com/influxdata/telegraf/issues/2061): Fix panic when file stat info cannot be collected due to permissions or other issue(s).
|
||||
- [#2045](https://github.com/influxdata/telegraf/issues/2045): Graylog output should set short_message field.
|
||||
- [#1904](https://github.com/influxdata/telegraf/issues/1904): Hddtemp always put the value in the field temperature.
|
||||
@@ -221,8 +289,11 @@ which can be installed via
|
||||
evaluated at every flush interval, rather than once at startup. This makes it
|
||||
consistent with the behavior of `collection_jitter`.
|
||||
|
||||
- postgresql plugins now handle oid and name typed columns seamlessly, previously they were ignored/skipped.
|
||||
|
||||
### Features
|
||||
|
||||
- [#1617](https://github.com/influxdata/telegraf/pull/1617): postgresql_extensible now handles name and oid types correctly.
|
||||
- [#1413](https://github.com/influxdata/telegraf/issues/1413): Separate container_version from container_image tag.
|
||||
- [#1525](https://github.com/influxdata/telegraf/pull/1525): Support setting per-device and total metrics for Docker network and blockio.
|
||||
- [#1466](https://github.com/influxdata/telegraf/pull/1466): MongoDB input plugin: adding per DB stats from db.stats()
|
||||
|
||||
111
Godeps
111
Godeps
@@ -1,65 +1,66 @@
|
||||
github.com/Shopify/sarama 8aadb476e66ca998f2f6bb3c993e9a2daa3666b9
|
||||
github.com/Sirupsen/logrus 219c8cb75c258c552e999735be6df753ffc7afdc
|
||||
github.com/aerospike/aerospike-client-go 7f3a312c3b2a60ac083ec6da296091c52c795c63
|
||||
github.com/amir/raidman 53c1b967405155bfc8758557863bf2e14f814687
|
||||
github.com/aws/aws-sdk-go 13a12060f716145019378a10e2806c174356b857
|
||||
github.com/beorn7/perks 3ac7bf7a47d159a033b107610db8a1b6575507a4
|
||||
github.com/cenkalti/backoff 4dc77674aceaabba2c7e3da25d4c823edfb73f99
|
||||
github.com/couchbase/go-couchbase cb664315a324d87d19c879d9cc67fda6be8c2ac1
|
||||
github.com/couchbase/gomemcached a5ea6356f648fec6ab89add00edd09151455b4b2
|
||||
github.com/Shopify/sarama 574d3147eee384229bf96a5d12c207fe7b5234f3
|
||||
github.com/Sirupsen/logrus 61e43dc76f7ee59a82bdf3d71033dc12bea4c77d
|
||||
github.com/aerospike/aerospike-client-go 95e1ad7791bdbca44707fedbb29be42024900d9c
|
||||
github.com/amir/raidman c74861fe6a7bb8ede0a010ce4485bdbb4fc4c985
|
||||
github.com/aws/aws-sdk-go 7524cb911daddd6e5c9195def8e59ae892bef8d9
|
||||
github.com/beorn7/perks 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9
|
||||
github.com/cenkalti/backoff b02f2bbce11d7ea6b97f282ef1771b0fe2f65ef3
|
||||
github.com/couchbase/go-couchbase bfe555a140d53dc1adf390f1a1d4b0fd4ceadb28
|
||||
github.com/couchbase/gomemcached 4a25d2f4e1dea9ea7dd76dfd943407abf9b07d29
|
||||
github.com/couchbase/goutils 5823a0cbaaa9008406021dc5daf80125ea30bba6
|
||||
github.com/dancannon/gorethink e7cac92ea2bc52638791a021f212145acfedb1fc
|
||||
github.com/davecgh/go-spew 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
|
||||
github.com/docker/engine-api 8924d6900370b4c7e7984be5adc61f50a80d7537
|
||||
github.com/docker/go-connections f549a9393d05688dff0992ef3efd8bbe6c628aeb
|
||||
github.com/docker/go-units 5d2041e26a699eaca682e2ea41c8f891e1060444
|
||||
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
|
||||
github.com/docker/distribution fb0bebc4b64e3881cc52a2478d749845ed76d2a8
|
||||
github.com/docker/engine-api 4290f40c056686fcaa5c9caf02eac1dde9315adf
|
||||
github.com/docker/go-connections 9670439d95da2651d9dfc7acc5d2ed92d3f25ee6
|
||||
github.com/docker/go-units 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
|
||||
github.com/eapache/go-resiliency b86b1ec0dd4209a588dc1285cdd471e73525c0b3
|
||||
github.com/eapache/queue ded5959c0d4e360646dc9e9908cff48666781367
|
||||
github.com/eclipse/paho.mqtt.golang 0f7a459f04f13a41b7ed752d47944528d4bf9a86
|
||||
github.com/go-sql-driver/mysql 1fca743146605a172a266e1654e01e5cd5669bee
|
||||
github.com/gobwas/glob 49571a1557cd20e6a2410adc6421f85b66c730b5
|
||||
github.com/golang/protobuf 552c7b9542c194800fd493123b3798ef0a832032
|
||||
github.com/golang/snappy d9eb7a3d35ec988b8585d4a0068e462c27d28380
|
||||
github.com/gorilla/context 1ea25387ff6f684839d82767c1733ff4d4d15d0a
|
||||
github.com/gorilla/mux c9e326e2bdec29039a3761c07bece13133863e1e
|
||||
github.com/eapache/go-xerial-snappy bb955e01b9346ac19dc29eb16586c90ded99a98c
|
||||
github.com/eapache/queue 44cc805cf13205b55f69e14bcb69867d1ae92f98
|
||||
github.com/eclipse/paho.mqtt.golang d4f545eb108a2d19f9b1a735689dbfb719bc21fb
|
||||
github.com/go-sql-driver/mysql 2e00b5cd70399450106cec6431c2e2ce3cae5034
|
||||
github.com/gobwas/glob bea32b9cd2d6f55753d94a28e959b13f0244797a
|
||||
github.com/golang/protobuf 8ee79997227bf9b34611aee7946ae64735e6fd93
|
||||
github.com/golang/snappy 7db9049039a047d955fe8c19b83c8ff5abd765c7
|
||||
github.com/gorilla/mux 392c28fe23e1c45ddba891b0320b3b5df220beea
|
||||
github.com/hailocab/go-hostpool e80d13ce29ede4452c43dea11e79b9bc8a15b478
|
||||
github.com/hashicorp/consul 5aa90455ce78d4d41578bafc86305e6e6b28d7d2
|
||||
github.com/hpcloud/tail b2940955ab8b26e19d43a43c4da0475dd81bdb56
|
||||
github.com/influxdata/config b79f6829346b8d6e78ba73544b1e1038f1f1c9da
|
||||
github.com/influxdata/influxdb fc57c0f7c635df3873f3d64f0ed2100ddc94d5ae
|
||||
github.com/influxdata/toml af4df43894b16e3fd2b788d01bd27ad0776ef2d0
|
||||
github.com/hashicorp/consul 63d2fc68239b996096a1c55a0d4b400ea4c2583f
|
||||
github.com/hpcloud/tail 915e5feba042395f5fda4dbe9c0e99aeab3088b3
|
||||
github.com/influxdata/config 8ec4638a81500c20be24855812bc8498ebe2dc92
|
||||
github.com/influxdata/toml ad49a5c2936f96b8f5943c3fdba47630ccf45a0d
|
||||
github.com/influxdata/wlog 7c63b0a71ef8300adc255344d275e10e5c3a71ec
|
||||
github.com/kardianos/osext 29ae4ffbc9a6fe9fb2bc5029050ce6996ea1d3bc
|
||||
github.com/kardianos/service 5e335590050d6d00f3aa270217d288dda1c94d0a
|
||||
github.com/jackc/pgx c8080fc4a1bfa44bf90383ad0fdce2f68b7d313c
|
||||
github.com/kardianos/osext c2c54e542fb797ad986b31721e1baedf214ca413
|
||||
github.com/kardianos/service 6d3a0ee7d3425d9d835debc51a0ca1ffa28f4893
|
||||
github.com/kballard/go-shellquote d8ec1a69a250a17bb0e419c386eac1f3711dc142
|
||||
github.com/klauspost/crc32 19b0b332c9e4516a6370a0456e6182c3b5036720
|
||||
github.com/lib/pq e182dc4027e2ded4b19396d638610f2653295f36
|
||||
github.com/matttproud/golang_protobuf_extensions d0c3fe89de86839aecf2e0579c40ba3bb336a453
|
||||
github.com/miekg/dns cce6c130cdb92c752850880fd285bea1d64439dd
|
||||
github.com/mreiferson/go-snappystream 028eae7ab5c4c9e2d1cb4c4ca1e53259bbe7e504
|
||||
github.com/klauspost/crc32 cb6bfca970f6908083f26f39a79009d608efd5cd
|
||||
github.com/matttproud/golang_protobuf_extensions c12348ce28de40eed0136aa2b644d0ee0650e56c
|
||||
github.com/miekg/dns 99f84ae56e75126dd77e5de4fae2ea034a468ca1
|
||||
github.com/naoina/go-stringutil 6b638e95a32d0c1131db0e7fe83775cbea4a0d0b
|
||||
github.com/nats-io/nats ea8b4fd12ebb823073c0004b9f09ac8748f4f165
|
||||
github.com/nats-io/nuid a5152d67cf63cbfb5d992a395458722a45194715
|
||||
github.com/nsqio/go-nsq 0b80d6f05e15ca1930e0c5e1d540ed627e299980
|
||||
github.com/opencontainers/runc 89ab7f2ccc1e45ddf6485eaa802c35dcf321dfc8
|
||||
github.com/prometheus/client_golang 18acf9993a863f4c4b40612e19cdd243e7c86831
|
||||
github.com/nats-io/go-nats ea9585611a4ab58a205b9b125ebd74c389a6b898
|
||||
github.com/nats-io/nats ea9585611a4ab58a205b9b125ebd74c389a6b898
|
||||
github.com/nats-io/nuid 289cccf02c178dc782430d534e3c1f5b72af807f
|
||||
github.com/nsqio/go-nsq a53d495e81424aaf7a7665a9d32a97715c40e953
|
||||
github.com/pierrec/lz4 5c9560bfa9ace2bf86080bf40d46b34ae44604df
|
||||
github.com/pierrec/xxHash 5a004441f897722c627870a981d02b29924215fa
|
||||
github.com/prometheus/client_golang c317fb74746eac4fc65fe3909195f4cf67c5562a
|
||||
github.com/prometheus/client_model fa8ad6fec33561be4280a8f0514318c79d7f6cb6
|
||||
github.com/prometheus/common e8eabff8812b05acf522b45fdcd725a785188e37
|
||||
github.com/prometheus/procfs 406e5b7bfd8201a36e2bb5f7bdae0b03380c2ce8
|
||||
github.com/samuel/go-zookeeper 218e9c81c0dd8b3b18172b2bbfad92cc7d6db55f
|
||||
github.com/shirou/gopsutil 1516eb9ddc5e61ba58874047a98f8b44b5e585e8
|
||||
github.com/soniah/gosnmp 3fe3beb30fa9700988893c56a63b1df8e1b68c26
|
||||
github.com/streadway/amqp b4f3ceab0337f013208d31348b578d83c0064744
|
||||
github.com/stretchr/testify 1f4a1643a57e798696635ea4c126e9127adb7d3c
|
||||
github.com/prometheus/common dd2f054febf4a6c00f2343686efb775948a8bff4
|
||||
github.com/prometheus/procfs 1878d9fbb537119d24b21ca07effd591627cd160
|
||||
github.com/rcrowley/go-metrics 1f30fe9094a513ce4c700b9a54458bbb0c96996c
|
||||
github.com/samuel/go-zookeeper 1d7be4effb13d2d908342d349d71a284a7542693
|
||||
github.com/shirou/gopsutil 77b5d0080adb6f028e457906f1944d9fcca34442
|
||||
github.com/soniah/gosnmp 5ad50dc75ab389f8a1c9f8a67d3a1cd85f67ed15
|
||||
github.com/streadway/amqp 63795daa9a446c920826655f26ba31c81c860fd6
|
||||
github.com/stretchr/testify 4d4bfba8f1d1027c4fdbe371823030df51419987
|
||||
github.com/vjeantet/grok 83bfdfdfd1a8146795b28e547a8e3c8b28a466c2
|
||||
github.com/wvanbergen/kafka bc265fedb9ff5b5c5d3c0fdcef4a819b3523d3ee
|
||||
github.com/wvanbergen/kazoo-go 0f768712ae6f76454f987c3356177e138df258f8
|
||||
github.com/yuin/gopher-lua bf3808abd44b1e55143a2d7f08571aaa80db1808
|
||||
github.com/wvanbergen/kazoo-go 968957352185472eacb69215fa3dbfcfdbac1096
|
||||
github.com/yuin/gopher-lua 66c871e454fcf10251c61bf8eff02d0978cae75a
|
||||
github.com/zensqlmonitor/go-mssqldb ffe5510c6fa5e15e6d983210ab501c815b56b363
|
||||
golang.org/x/crypto c197bcf24cde29d3f73c7b4ac6fd41f4384e8af6
|
||||
golang.org/x/net 6acef71eb69611914f7a30939ea9f6e194c78172
|
||||
golang.org/x/text a71fd10341b064c10f4a81ceac72bcf70f26ea34
|
||||
gopkg.in/dancannon/gorethink.v1 7d1af5be49cb5ecc7b177bf387d232050299d6ef
|
||||
gopkg.in/fatih/pool.v2 cba550ebf9bce999a02e963296d4bc7a486cb715
|
||||
gopkg.in/mgo.v2 d90005c5262a3463800497ea5a89aed5fe22c886
|
||||
gopkg.in/yaml.v2 a83829b6f1293c91addabc89d0571c246397bbf4
|
||||
golang.org/x/crypto dc137beb6cce2043eb6b5f223ab8bf51c32459f4
|
||||
golang.org/x/net f2499483f923065a842d38eb4c7f1927e6fc6e6d
|
||||
golang.org/x/text 506f9d5c962f284575e88337e7d9296d27e729d3
|
||||
gopkg.in/dancannon/gorethink.v1 edc7a6a68e2d8015f5ffe1b2560eed989f8a45be
|
||||
gopkg.in/fatih/pool.v2 6e328e67893eb46323ad06f0e92cb9536babbabc
|
||||
gopkg.in/mgo.v2 3f83fa5005286a7fe593b055f0d7771a7dce4655
|
||||
gopkg.in/yaml.v2 4c78c975fe7c825c6d1466c42be594d1d6f3aba6
|
||||
|
||||
4
Makefile
4
Makefile
@@ -58,7 +58,7 @@ docker-run:
|
||||
docker run --name redis -p "6379:6379" -d redis
|
||||
docker run --name nsq -p "4150:4150" -d nsqio/nsq /nsqd
|
||||
docker run --name mqtt -p "1883:1883" -d ncarlier/mqtt
|
||||
docker run --name riemann -p "5555:5555" -d blalor/riemann
|
||||
docker run --name riemann -p "5555:5555" -d stealthly/docker-riemann
|
||||
docker run --name nats -p "4222:4222" -d nats
|
||||
|
||||
# Run docker containers necessary for CircleCI unit tests
|
||||
@@ -71,7 +71,7 @@ docker-run-circle:
|
||||
-d spotify/kafka
|
||||
docker run --name nsq -p "4150:4150" -d nsqio/nsq /nsqd
|
||||
docker run --name mqtt -p "1883:1883" -d ncarlier/mqtt
|
||||
docker run --name riemann -p "5555:5555" -d blalor/riemann
|
||||
docker run --name riemann -p "5555:5555" -d stealthly/docker-riemann
|
||||
docker run --name nats -p "4222:4222" -d nats
|
||||
|
||||
# Kill all docker containers, ignore errors
|
||||
|
||||
64
README.md
64
README.md
@@ -25,60 +25,20 @@ new plugins.
|
||||
|
||||
## Installation:
|
||||
|
||||
### Linux deb and rpm Packages:
|
||||
You can either download the binaries directly from the
|
||||
[downloads](https://www.influxdata.com/downloads) page.
|
||||
|
||||
Latest:
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf_1.1.1_amd64.deb
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf-1.1.1.x86_64.rpm
|
||||
|
||||
Latest (arm):
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf_1.1.1_armhf.deb
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf-1.1.1.armhf.rpm
|
||||
|
||||
##### Package Instructions:
|
||||
|
||||
* Telegraf binary is installed in `/usr/bin/telegraf`
|
||||
* Telegraf daemon configuration file is in `/etc/telegraf/telegraf.conf`
|
||||
* On sysv systems, the telegraf daemon can be controlled via
|
||||
`service telegraf [action]`
|
||||
* On systemd systems (such as Ubuntu 15+), the telegraf daemon can be
|
||||
controlled via `systemctl [action] telegraf`
|
||||
|
||||
### yum/apt Repositories:
|
||||
|
||||
There is a yum/apt repo available for the whole InfluxData stack, see
|
||||
[here](https://docs.influxdata.com/influxdb/latest/introduction/installation/#installation)
|
||||
for instructions on setting up the repo. Once it is configured, you will be able
|
||||
to use this repo to install & update telegraf.
|
||||
|
||||
### Linux tarballs:
|
||||
|
||||
Latest:
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf-1.1.1_linux_amd64.tar.gz
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf-1.1.1_linux_i386.tar.gz
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf-1.1.1_linux_armhf.tar.gz
|
||||
A few alternate installs are available here as well:
|
||||
|
||||
### FreeBSD tarball:
|
||||
|
||||
Latest:
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf-1.1.1_freebsd_amd64.tar.gz
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf-VERSION_freebsd_amd64.tar.gz
|
||||
|
||||
### Ansible Role:
|
||||
|
||||
Ansible role: https://github.com/rossmcdonald/telegraf
|
||||
|
||||
### OSX via Homebrew:
|
||||
|
||||
```
|
||||
brew update
|
||||
brew install telegraf
|
||||
```
|
||||
|
||||
### Windows Binaries (EXPERIMENTAL)
|
||||
|
||||
Latest:
|
||||
* https://dl.influxdata.com/telegraf/releases/telegraf-1.1.1_windows_amd64.zip
|
||||
|
||||
### From Source:
|
||||
|
||||
Telegraf manages dependencies via [gdm](https://github.com/sparrc/gdm),
|
||||
@@ -99,31 +59,31 @@ See usage with:
|
||||
telegraf --help
|
||||
```
|
||||
|
||||
### Generate a telegraf config file:
|
||||
#### Generate a telegraf config file:
|
||||
|
||||
```
|
||||
telegraf config > telegraf.conf
|
||||
```
|
||||
|
||||
### Generate config with only cpu input & influxdb output plugins defined
|
||||
#### Generate config with only cpu input & influxdb output plugins defined
|
||||
|
||||
```
|
||||
telegraf --input-filter cpu --output-filter influxdb config
|
||||
```
|
||||
|
||||
### Run a single telegraf collection, outputing metrics to stdout
|
||||
#### Run a single telegraf collection, outputing metrics to stdout
|
||||
|
||||
```
|
||||
telegraf --config telegraf.conf -test
|
||||
```
|
||||
|
||||
### Run telegraf with all plugins defined in config file
|
||||
#### Run telegraf with all plugins defined in config file
|
||||
|
||||
```
|
||||
telegraf --config telegraf.conf
|
||||
```
|
||||
|
||||
### Run telegraf, enabling the cpu & memory input, and influxdb output plugins
|
||||
#### Run telegraf, enabling the cpu & memory input, and influxdb output plugins
|
||||
|
||||
```
|
||||
telegraf --config telegraf.conf -input-filter cpu:mem -output-filter influxdb
|
||||
@@ -164,6 +124,7 @@ configuration options.
|
||||
* [ipmi_sensor](./plugins/inputs/ipmi_sensor)
|
||||
* [iptables](./plugins/inputs/iptables)
|
||||
* [jolokia](./plugins/inputs/jolokia)
|
||||
* [kubernetes](./plugins/inputs/kubernetes)
|
||||
* [leofs](./plugins/inputs/leofs)
|
||||
* [lustre2](./plugins/inputs/lustre2)
|
||||
* [mailchimp](./plugins/inputs/mailchimp)
|
||||
@@ -221,6 +182,7 @@ Telegraf can also collect metrics via the following service plugins:
|
||||
* [nsq_consumer](./plugins/inputs/nsq_consumer)
|
||||
* [logparser](./plugins/inputs/logparser)
|
||||
* [statsd](./plugins/inputs/statsd)
|
||||
* [socket_listener](./plugins/inputs/socket_listener)
|
||||
* [tail](./plugins/inputs/tail)
|
||||
* [tcp_listener](./plugins/inputs/tcp_listener)
|
||||
* [udp_listener](./plugins/inputs/udp_listener)
|
||||
@@ -242,7 +204,7 @@ Telegraf can also collect metrics via the following service plugins:
|
||||
|
||||
* [influxdb](./plugins/outputs/influxdb)
|
||||
* [amon](./plugins/outputs/amon)
|
||||
* [amqp](./plugins/outputs/amqp)
|
||||
* [amqp](./plugins/outputs/amqp) (rabbitmq)
|
||||
* [aws kinesis](./plugins/outputs/kinesis)
|
||||
* [aws cloudwatch](./plugins/outputs/cloudwatch)
|
||||
* [datadog](./plugins/outputs/datadog)
|
||||
@@ -258,7 +220,9 @@ Telegraf can also collect metrics via the following service plugins:
|
||||
* [nsq](./plugins/outputs/nsq)
|
||||
* [opentsdb](./plugins/outputs/opentsdb)
|
||||
* [prometheus](./plugins/outputs/prometheus_client)
|
||||
* [socket_writer](./plugins/outputs/socket_writer)
|
||||
* [riemann](./plugins/outputs/riemann)
|
||||
* [riemann_legacy](./plugins/outputs/riemann_legacy)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -157,13 +157,13 @@ func gatherWithTimeout(
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
log.Printf("E! ERROR in input [%s]: %s", input.Name(), err)
|
||||
acc.AddError(err)
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
log.Printf("E! ERROR: input [%s] took longer to collect than "+
|
||||
"collection interval (%s)",
|
||||
input.Name(), timeout)
|
||||
err := fmt.Errorf("took longer to collect than collection interval (%s)",
|
||||
timeout)
|
||||
acc.AddError(err)
|
||||
continue
|
||||
case <-shutdown:
|
||||
return
|
||||
@@ -286,6 +286,7 @@ func (a *Agent) flusher(shutdown chan struct{}, metricC chan telegraf.Metric) er
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(a.Config.Agent.FlushInterval.Duration)
|
||||
semaphore := make(chan struct{}, 1)
|
||||
for {
|
||||
select {
|
||||
case <-shutdown:
|
||||
@@ -295,8 +296,18 @@ func (a *Agent) flusher(shutdown chan struct{}, metricC chan telegraf.Metric) er
|
||||
a.flush()
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
internal.RandomSleep(a.Config.Agent.FlushJitter.Duration, shutdown)
|
||||
a.flush()
|
||||
go func() {
|
||||
select {
|
||||
case semaphore <- struct{}{}:
|
||||
internal.RandomSleep(a.Config.Agent.FlushJitter.Duration, shutdown)
|
||||
a.flush()
|
||||
<-semaphore
|
||||
default:
|
||||
// skipping this flush because one is already happening
|
||||
log.Println("W! Skipping a scheduled flush because there is" +
|
||||
" already a flush ongoing.")
|
||||
}
|
||||
}()
|
||||
case metric := <-metricC:
|
||||
// NOTE potential bottleneck here as we put each metric through the
|
||||
// processors serially.
|
||||
|
||||
@@ -4,9 +4,9 @@ machine:
|
||||
post:
|
||||
- sudo service zookeeper stop
|
||||
- go version
|
||||
- go version | grep 1.7.4 || sudo rm -rf /usr/local/go
|
||||
- wget https://storage.googleapis.com/golang/go1.7.4.linux-amd64.tar.gz
|
||||
- sudo tar -C /usr/local -xzf go1.7.4.linux-amd64.tar.gz
|
||||
- sudo rm -rf /usr/local/go
|
||||
- wget https://storage.googleapis.com/golang/go1.8rc3.linux-amd64.tar.gz
|
||||
- sudo tar -C /usr/local -xzf go1.8rc3.linux-amd64.tar.gz
|
||||
- go version
|
||||
|
||||
dependencies:
|
||||
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -50,6 +53,8 @@ var fUsage = flag.String("usage", "",
|
||||
"print usage for a plugin, ie, 'telegraf -usage mysql'")
|
||||
var fService = flag.String("service", "",
|
||||
"operate on the service")
|
||||
var fPlugins = flag.String("plugins", "",
|
||||
"path to directory containing external plugins")
|
||||
|
||||
// Telegraf version, populated linker.
|
||||
// ie, -ldflags "-X main.version=`git describe --always --tags`"
|
||||
@@ -109,94 +114,17 @@ Examples:
|
||||
|
||||
var stop chan struct{}
|
||||
|
||||
var srvc service.Service
|
||||
|
||||
type program struct{}
|
||||
|
||||
func reloadLoop(stop chan struct{}, s service.Service) {
|
||||
defer func() {
|
||||
if service.Interactive() {
|
||||
os.Exit(0)
|
||||
}
|
||||
return
|
||||
}()
|
||||
func reloadLoop(
|
||||
stop chan struct{},
|
||||
inputFilters []string,
|
||||
outputFilters []string,
|
||||
aggregatorFilters []string,
|
||||
processorFilters []string,
|
||||
) {
|
||||
reload := make(chan bool, 1)
|
||||
reload <- true
|
||||
for <-reload {
|
||||
reload <- false
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
var inputFilters []string
|
||||
if *fInputFilters != "" {
|
||||
inputFilter := strings.TrimSpace(*fInputFilters)
|
||||
inputFilters = strings.Split(":"+inputFilter+":", ":")
|
||||
}
|
||||
var outputFilters []string
|
||||
if *fOutputFilters != "" {
|
||||
outputFilter := strings.TrimSpace(*fOutputFilters)
|
||||
outputFilters = strings.Split(":"+outputFilter+":", ":")
|
||||
}
|
||||
var aggregatorFilters []string
|
||||
if *fAggregatorFilters != "" {
|
||||
aggregatorFilter := strings.TrimSpace(*fAggregatorFilters)
|
||||
aggregatorFilters = strings.Split(":"+aggregatorFilter+":", ":")
|
||||
}
|
||||
var processorFilters []string
|
||||
if *fProcessorFilters != "" {
|
||||
processorFilter := strings.TrimSpace(*fProcessorFilters)
|
||||
processorFilters = strings.Split(":"+processorFilter+":", ":")
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
switch args[0] {
|
||||
case "version":
|
||||
fmt.Printf("Telegraf v%s (git: %s %s)\n", version, branch, commit)
|
||||
return
|
||||
case "config":
|
||||
config.PrintSampleConfig(
|
||||
inputFilters,
|
||||
outputFilters,
|
||||
aggregatorFilters,
|
||||
processorFilters,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// switch for flags which just do something and exit immediately
|
||||
switch {
|
||||
case *fOutputList:
|
||||
fmt.Println("Available Output Plugins:")
|
||||
for k, _ := range outputs.Outputs {
|
||||
fmt.Printf(" %s\n", k)
|
||||
}
|
||||
return
|
||||
case *fInputList:
|
||||
fmt.Println("Available Input Plugins:")
|
||||
for k, _ := range inputs.Inputs {
|
||||
fmt.Printf(" %s\n", k)
|
||||
}
|
||||
return
|
||||
case *fVersion:
|
||||
fmt.Printf("Telegraf v%s (git: %s %s)\n", version, branch, commit)
|
||||
return
|
||||
case *fSampleConfig:
|
||||
config.PrintSampleConfig(
|
||||
inputFilters,
|
||||
outputFilters,
|
||||
aggregatorFilters,
|
||||
processorFilters,
|
||||
)
|
||||
return
|
||||
case *fUsage != "":
|
||||
if err := config.PrintInputConfig(*fUsage); err != nil {
|
||||
if err2 := config.PrintOutputConfig(*fUsage); err2 != nil {
|
||||
log.Fatalf("E! %s and %s", err, err2)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If no other options are specified, load the config file and run.
|
||||
c := config.NewConfig()
|
||||
@@ -237,7 +165,7 @@ func reloadLoop(stop chan struct{}, s service.Service) {
|
||||
if err != nil {
|
||||
log.Fatal("E! " + err.Error())
|
||||
}
|
||||
return
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err = ag.Connect()
|
||||
@@ -271,14 +199,21 @@ func reloadLoop(stop chan struct{}, s service.Service) {
|
||||
log.Printf("I! Tags enabled: %s", c.ListTags())
|
||||
|
||||
if *fPidfile != "" {
|
||||
f, err := os.Create(*fPidfile)
|
||||
f, err := os.OpenFile(*fPidfile, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("E! Unable to create pidfile: %s", err)
|
||||
log.Printf("E! Unable to create pidfile: %s", err)
|
||||
} else {
|
||||
fmt.Fprintf(f, "%d\n", os.Getpid())
|
||||
|
||||
f.Close()
|
||||
|
||||
defer func() {
|
||||
err := os.Remove(*fPidfile)
|
||||
if err != nil {
|
||||
log.Printf("E! Unable to remove pidfile: %s", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
fmt.Fprintf(f, "%d\n", os.Getpid())
|
||||
|
||||
f.Close()
|
||||
}
|
||||
|
||||
ag.Run(shutdown)
|
||||
@@ -290,23 +225,145 @@ func usageExit(rc int) {
|
||||
os.Exit(rc)
|
||||
}
|
||||
|
||||
type program struct {
|
||||
inputFilters []string
|
||||
outputFilters []string
|
||||
aggregatorFilters []string
|
||||
processorFilters []string
|
||||
}
|
||||
|
||||
func (p *program) Start(s service.Service) error {
|
||||
srvc = s
|
||||
go p.run()
|
||||
return nil
|
||||
}
|
||||
func (p *program) run() {
|
||||
stop = make(chan struct{})
|
||||
reloadLoop(stop, srvc)
|
||||
reloadLoop(
|
||||
stop,
|
||||
p.inputFilters,
|
||||
p.outputFilters,
|
||||
p.aggregatorFilters,
|
||||
p.processorFilters,
|
||||
)
|
||||
}
|
||||
func (p *program) Stop(s service.Service) error {
|
||||
close(stop)
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadExternalPlugins loads external plugins from shared libraries (.so, .dll, etc.)
|
||||
// in the specified directory.
|
||||
func loadExternalPlugins(dir string) error {
|
||||
return filepath.Walk(dir, func(pth string, info os.FileInfo, err error) error {
|
||||
// Stop if there was an error.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ignore directories.
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ignore files that aren't shared libraries.
|
||||
ext := strings.ToLower(path.Ext(pth))
|
||||
if ext != ".so" && ext != ".dll" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load plugin.
|
||||
_, err = plugin.Open(pth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening [%s]: %s", pth, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() { usageExit(0) }
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
// Load external plugins, if requested.
|
||||
if *fPlugins != "" {
|
||||
pluginsDir, err := filepath.Abs(*fPlugins)
|
||||
if err != nil {
|
||||
log.Fatal("E! " + err.Error())
|
||||
}
|
||||
log.Printf("I! Loading external plugins from: %s\n", pluginsDir)
|
||||
if err := loadExternalPlugins(*fPlugins); err != nil {
|
||||
log.Fatal("E! " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
inputFilters, outputFilters := []string{}, []string{}
|
||||
if *fInputFilters != "" {
|
||||
inputFilters = strings.Split(":"+strings.TrimSpace(*fInputFilters)+":", ":")
|
||||
}
|
||||
if *fOutputFilters != "" {
|
||||
outputFilters = strings.Split(":"+strings.TrimSpace(*fOutputFilters)+":", ":")
|
||||
}
|
||||
|
||||
aggregatorFilters, processorFilters := []string{}, []string{}
|
||||
if *fAggregatorFilters != "" {
|
||||
aggregatorFilters = strings.Split(":"+strings.TrimSpace(*fAggregatorFilters)+":", ":")
|
||||
}
|
||||
if *fProcessorFilters != "" {
|
||||
processorFilters = strings.Split(":"+strings.TrimSpace(*fProcessorFilters)+":", ":")
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
switch args[0] {
|
||||
case "version":
|
||||
fmt.Printf("Telegraf v%s (git: %s %s)\n", version, branch, commit)
|
||||
return
|
||||
case "config":
|
||||
config.PrintSampleConfig(
|
||||
inputFilters,
|
||||
outputFilters,
|
||||
aggregatorFilters,
|
||||
processorFilters,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// switch for flags which just do something and exit immediately
|
||||
switch {
|
||||
case *fOutputList:
|
||||
fmt.Println("Available Output Plugins:")
|
||||
for k, _ := range outputs.Outputs {
|
||||
fmt.Printf(" %s\n", k)
|
||||
}
|
||||
return
|
||||
case *fInputList:
|
||||
fmt.Println("Available Input Plugins:")
|
||||
for k, _ := range inputs.Inputs {
|
||||
fmt.Printf(" %s\n", k)
|
||||
}
|
||||
return
|
||||
case *fVersion:
|
||||
fmt.Printf("Telegraf v%s (git: %s %s)\n", version, branch, commit)
|
||||
return
|
||||
case *fSampleConfig:
|
||||
config.PrintSampleConfig(
|
||||
inputFilters,
|
||||
outputFilters,
|
||||
aggregatorFilters,
|
||||
processorFilters,
|
||||
)
|
||||
return
|
||||
case *fUsage != "":
|
||||
err := config.PrintInputConfig(*fUsage)
|
||||
err2 := config.PrintOutputConfig(*fUsage)
|
||||
if err != nil && err2 != nil {
|
||||
log.Fatalf("E! %s and %s", err, err2)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
svcConfig := &service.Config{
|
||||
Name: "telegraf",
|
||||
@@ -316,7 +373,12 @@ func main() {
|
||||
Arguments: []string{"-config", "C:\\Program Files\\Telegraf\\telegraf.conf"},
|
||||
}
|
||||
|
||||
prg := &program{}
|
||||
prg := &program{
|
||||
inputFilters: inputFilters,
|
||||
outputFilters: outputFilters,
|
||||
aggregatorFilters: aggregatorFilters,
|
||||
processorFilters: processorFilters,
|
||||
}
|
||||
s, err := service.New(prg, svcConfig)
|
||||
if err != nil {
|
||||
log.Fatal("E! " + err.Error())
|
||||
@@ -327,10 +389,14 @@ func main() {
|
||||
if *fConfig != "" {
|
||||
(*svcConfig).Arguments = []string{"-config", *fConfig}
|
||||
}
|
||||
if *fConfigDirectory != "" {
|
||||
(*svcConfig).Arguments = append((*svcConfig).Arguments, "-config-directory", *fConfigDirectory)
|
||||
}
|
||||
err := service.Control(s, *fService)
|
||||
if err != nil {
|
||||
log.Fatal("E! " + err.Error())
|
||||
}
|
||||
os.Exit(0)
|
||||
} else {
|
||||
err = s.Run()
|
||||
if err != nil {
|
||||
@@ -339,6 +405,12 @@ func main() {
|
||||
}
|
||||
} else {
|
||||
stop = make(chan struct{})
|
||||
reloadLoop(stop, nil)
|
||||
reloadLoop(
|
||||
stop,
|
||||
inputFilters,
|
||||
outputFilters,
|
||||
aggregatorFilters,
|
||||
processorFilters,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,8 +443,39 @@
|
||||
# # expiration_interval = "60s"
|
||||
|
||||
|
||||
# # Configuration for the Riemann server to send metrics to
|
||||
# # Configuration for Riemann server to send metrics to
|
||||
# [[outputs.riemann]]
|
||||
# ## The full TCP or UDP URL of the Riemann server
|
||||
# url = "tcp://localhost:5555"
|
||||
#
|
||||
# ## Riemann event TTL, floating-point time in seconds.
|
||||
# ## Defines how long that an event is considered valid for in Riemann
|
||||
# # ttl = 30.0
|
||||
#
|
||||
# ## Separator to use between measurement and field name in Riemann service name
|
||||
# ## This does not have any effect if 'measurement_as_attribute' is set to 'true'
|
||||
# separator = "/"
|
||||
#
|
||||
# ## Set measurement name as Riemann attribute 'measurement', instead of prepending it to the Riemann service name
|
||||
# # measurement_as_attribute = false
|
||||
#
|
||||
# ## Send string metrics as Riemann event states.
|
||||
# ## Unless enabled all string metrics will be ignored
|
||||
# # string_as_state = false
|
||||
#
|
||||
# ## A list of tag keys whose values get sent as Riemann tags.
|
||||
# ## If empty, all Telegraf tag values will be sent as tags
|
||||
# # tag_keys = ["telegraf","custom_tag"]
|
||||
#
|
||||
# ## Additional Riemann tags to send.
|
||||
# # tags = ["telegraf-output"]
|
||||
#
|
||||
# ## Description for Riemann event
|
||||
# # description_text = "metrics collected from telegraf"
|
||||
|
||||
|
||||
# # Configuration for the legacy Riemann plugin
|
||||
# [[outputs.riemann_legacy]]
|
||||
# ## URL of server
|
||||
# url = "localhost:5555"
|
||||
# ## transport protocol to use either tcp or udp
|
||||
|
||||
@@ -105,10 +105,11 @@
|
||||
"% Privileged Time",
|
||||
"% User Time",
|
||||
"% Processor Time",
|
||||
"% DPC Time",
|
||||
]
|
||||
Measurement = "win_cpu"
|
||||
# Set to true to include _Total instance when querying for all (*).
|
||||
#IncludeTotal=false
|
||||
IncludeTotal=true
|
||||
|
||||
[[inputs.win_perf_counters.object]]
|
||||
# Disk times and queues
|
||||
@@ -118,19 +119,51 @@
|
||||
"% Idle Time",
|
||||
"% Disk Time","% Disk Read Time",
|
||||
"% Disk Write Time",
|
||||
"% User Time",
|
||||
"Current Disk Queue Length",
|
||||
"% Free Space",
|
||||
"Free Megabytes",
|
||||
]
|
||||
Measurement = "win_disk"
|
||||
# Set to true to include _Total instance when querying for all (*).
|
||||
#IncludeTotal=false
|
||||
|
||||
[[inputs.win_perf_counters.object]]
|
||||
ObjectName = "PhysicalDisk"
|
||||
Instances = ["*"]
|
||||
Counters = [
|
||||
"Disk Read Bytes/sec",
|
||||
"Disk Write Bytes/sec",
|
||||
"Current Disk Queue Length",
|
||||
"Disk Reads/sec",
|
||||
"Disk Writes/sec",
|
||||
"% Disk Time",
|
||||
"% Disk Read Time",
|
||||
"% Disk Write Time",
|
||||
]
|
||||
Measurement = "win_diskio"
|
||||
|
||||
[[inputs.win_perf_counters.object]]
|
||||
ObjectName = "Network Interface"
|
||||
Instances = ["*"]
|
||||
Counters = [
|
||||
"Bytes Received/sec",
|
||||
"Bytes Sent/sec",
|
||||
"Packets Received/sec",
|
||||
"Packets Sent/sec",
|
||||
"Packets Received Discarded",
|
||||
"Packets Outbound Discarded",
|
||||
"Packets Received Errors",
|
||||
"Packets Outbound Errors",
|
||||
]
|
||||
Measurement = "win_net"
|
||||
|
||||
[[inputs.win_perf_counters.object]]
|
||||
ObjectName = "System"
|
||||
Counters = [
|
||||
"Context Switches/sec",
|
||||
"System Calls/sec",
|
||||
"Processor Queue Length",
|
||||
"System Up Time",
|
||||
]
|
||||
Instances = ["------"]
|
||||
Measurement = "win_system"
|
||||
@@ -150,6 +183,10 @@
|
||||
"Transition Faults/sec",
|
||||
"Pool Nonpaged Bytes",
|
||||
"Pool Paged Bytes",
|
||||
"Standby Cache Reserve Bytes",
|
||||
"Standby Cache Normal Priority Bytes",
|
||||
"Standby Cache Core Bytes",
|
||||
|
||||
]
|
||||
# Use 6 x - to remove the Instance bit from the query.
|
||||
Instances = ["------"]
|
||||
@@ -157,6 +194,31 @@
|
||||
# Set to true to include _Total instance when querying for all (*).
|
||||
#IncludeTotal=false
|
||||
|
||||
[[inputs.win_perf_counters.object]]
|
||||
# Example query where the Instance portion must be removed to get data back,
|
||||
# such as from the Paging File object.
|
||||
ObjectName = "Paging File"
|
||||
Counters = [
|
||||
"% Usage",
|
||||
]
|
||||
Instances = ["_Total"]
|
||||
Measurement = "win_swap"
|
||||
|
||||
[[inputs.win_perf_counters.object]]
|
||||
ObjectName = "Network Interface"
|
||||
Instances = ["*"]
|
||||
Counters = [
|
||||
"Bytes Sent/sec",
|
||||
"Bytes Received/sec",
|
||||
"Packets Sent/sec",
|
||||
"Packets Received/sec",
|
||||
"Packets Received Discarded",
|
||||
"Packets Received Errors",
|
||||
"Packets Outbound Discarded",
|
||||
"Packets Outbound Errors",
|
||||
]
|
||||
|
||||
|
||||
|
||||
# Windows system plugins using WMI (disabled by default, using
|
||||
# win_perf_counters over WMI is recommended)
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestCompileAndMatch(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
matches := g1.Match()
|
||||
assert.Len(t, matches, 3)
|
||||
assert.Len(t, matches, 6)
|
||||
matches = g2.Match()
|
||||
assert.Len(t, matches, 2)
|
||||
matches = g3.Match()
|
||||
@@ -56,6 +56,16 @@ func TestFindRootDir(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindNestedTextFile(t *testing.T) {
|
||||
dir := getTestdataDir()
|
||||
// test super asterisk
|
||||
g1, err := Compile(dir + "/**.txt")
|
||||
require.NoError(t, err)
|
||||
|
||||
matches := g1.Match()
|
||||
assert.Len(t, matches, 1)
|
||||
}
|
||||
|
||||
func getTestdataDir() string {
|
||||
_, filename, _, _ := runtime.Caller(1)
|
||||
return strings.Replace(filename, "globpath_test.go", "testdata", 1)
|
||||
|
||||
0
internal/globpath/testdata/nested1/nested2/nested.txt
vendored
Normal file
0
internal/globpath/testdata/nested1/nested2/nested.txt
vendored
Normal file
@@ -90,6 +90,9 @@ func NewRunningOutput(
|
||||
// AddMetric adds a metric to the output. This function can also write cached
|
||||
// points if FlushBufferWhenFull is true.
|
||||
func (ro *RunningOutput) AddMetric(m telegraf.Metric) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
// Filter any tagexclude/taginclude parameters before adding metric
|
||||
if ro.Config.Filter.IsActive() {
|
||||
// In order to filter out tags, we need to create a new metric, since
|
||||
|
||||
@@ -75,6 +75,23 @@ func BenchmarkRunningOutputAddFailWrites(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddingNilMetric(t *testing.T) {
|
||||
conf := &OutputConfig{
|
||||
Filter: Filter{},
|
||||
}
|
||||
|
||||
m := &mockOutput{}
|
||||
ro := NewRunningOutput("test", m, conf, 1000, 10000)
|
||||
|
||||
ro.AddMetric(nil)
|
||||
ro.AddMetric(nil)
|
||||
ro.AddMetric(nil)
|
||||
|
||||
err := ro.Write()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, m.Metrics(), 0)
|
||||
}
|
||||
|
||||
// Test that NameDrop filters ger properly applied.
|
||||
func TestRunningOutput_DropFilter(t *testing.T) {
|
||||
conf := &OutputConfig{
|
||||
|
||||
16
metric.go
16
metric.go
@@ -2,9 +2,6 @@ package telegraf
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
// TODO remove
|
||||
"github.com/influxdata/influxdb/client/v2"
|
||||
)
|
||||
|
||||
// ValueType is an enumeration of metric types that represent a simple value.
|
||||
@@ -19,8 +16,15 @@ const (
|
||||
)
|
||||
|
||||
type Metric interface {
|
||||
// Serialize serializes the metric into a line-protocol byte buffer,
|
||||
// including a newline at the end.
|
||||
Serialize() []byte
|
||||
String() string // convenience function for string(Serialize())
|
||||
// same as Serialize, but avoids an allocation.
|
||||
// returns number of bytes copied into dst.
|
||||
SerializeTo(dst []byte) int
|
||||
// String is the same as Serialize, but returns a string.
|
||||
String() string
|
||||
// Copy deep-copies the metric.
|
||||
Copy() Metric
|
||||
// Split will attempt to return multiple metrics with the same timestamp
|
||||
// whose string representations are no longer than maxSize.
|
||||
@@ -55,8 +59,4 @@ type Metric interface {
|
||||
// aggregator things:
|
||||
SetAggregate(bool)
|
||||
IsAggregate() bool
|
||||
|
||||
// Point returns a influxdb client.Point object
|
||||
// TODO remove this function
|
||||
Point() *client.Point
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
|
||||
// TODO remove
|
||||
"github.com/influxdata/influxdb/client/v2"
|
||||
)
|
||||
|
||||
const MaxInt = int(^uint(0) >> 1)
|
||||
@@ -137,11 +134,6 @@ type metric struct {
|
||||
nsec int64
|
||||
}
|
||||
|
||||
func (m *metric) Point() *client.Point {
|
||||
c, _ := client.NewPoint(m.Name(), m.Tags(), m.Fields(), m.Time())
|
||||
return c
|
||||
}
|
||||
|
||||
func (m *metric) String() string {
|
||||
return string(m.name) + string(m.tags) + " " + string(m.fields) + " " + string(m.t) + "\n"
|
||||
}
|
||||
@@ -178,6 +170,48 @@ func (m *metric) Serialize() []byte {
|
||||
return tmp
|
||||
}
|
||||
|
||||
func (m *metric) SerializeTo(dst []byte) int {
|
||||
i := 0
|
||||
if i >= len(dst) {
|
||||
return i
|
||||
}
|
||||
|
||||
i += copy(dst[i:], m.name)
|
||||
if i >= len(dst) {
|
||||
return i
|
||||
}
|
||||
|
||||
i += copy(dst[i:], m.tags)
|
||||
if i >= len(dst) {
|
||||
return i
|
||||
}
|
||||
|
||||
dst[i] = ' '
|
||||
i++
|
||||
if i >= len(dst) {
|
||||
return i
|
||||
}
|
||||
|
||||
i += copy(dst[i:], m.fields)
|
||||
if i >= len(dst) {
|
||||
return i
|
||||
}
|
||||
|
||||
dst[i] = ' '
|
||||
i++
|
||||
if i >= len(dst) {
|
||||
return i
|
||||
}
|
||||
|
||||
i += copy(dst[i:], m.t)
|
||||
if i >= len(dst) {
|
||||
return i
|
||||
}
|
||||
dst[i] = '\n'
|
||||
|
||||
return i + 1
|
||||
}
|
||||
|
||||
func (m *metric) Split(maxSize int) []telegraf.Metric {
|
||||
if m.Len() < maxSize {
|
||||
return []telegraf.Metric{m}
|
||||
@@ -263,7 +297,7 @@ func (m *metric) Fields() map[string]interface{} {
|
||||
case '"':
|
||||
// string field
|
||||
fieldMap[unescape(string(m.fields[i:][0:i1]), "fieldkey")] = unescape(string(m.fields[i:][i2+1:i3-1]), "fieldval")
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
// number field
|
||||
switch m.fields[i:][i3-1] {
|
||||
case 'i':
|
||||
|
||||
@@ -595,25 +595,6 @@ func TestNewMetricAggregate(t *testing.T) {
|
||||
assert.True(t, m.IsAggregate())
|
||||
}
|
||||
|
||||
func TestNewMetricPoint(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tags := map[string]string{
|
||||
"host": "localhost",
|
||||
}
|
||||
fields := map[string]interface{}{
|
||||
"usage_idle": float64(99),
|
||||
}
|
||||
m, err := New("cpu", tags, fields, now)
|
||||
assert.NoError(t, err)
|
||||
|
||||
p := m.Point()
|
||||
|
||||
assert.Equal(t, fields, m.Fields())
|
||||
assert.Equal(t, fields, p.Fields())
|
||||
assert.Equal(t, "cpu", p.Name())
|
||||
}
|
||||
|
||||
func TestNewMetricString(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ cpu,host=foo,datacenter=us-east idle=99,busy=1i,b=true,s="string"
|
||||
cpu,host=foo,datacenter=us-east idle=99,busy=1i,b=true,s="string"
|
||||
`
|
||||
|
||||
const negMetrics = `weather,host=local temp=-99i,temp_float=-99.4 1465839830100400200
|
||||
`
|
||||
|
||||
// some metrics are invalid
|
||||
const someInvalid = `cpu,host=foo,datacenter=us-east usage_idle=99,usage_busy=1
|
||||
cpu,host=foo,datacenter=us-east usage_idle=99,usage_busy=1
|
||||
@@ -85,6 +88,26 @@ func TestParse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNegNumbers(t *testing.T) {
|
||||
metrics, err := Parse([]byte(negMetrics))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, metrics, 1)
|
||||
|
||||
assert.Equal(t,
|
||||
map[string]interface{}{
|
||||
"temp": int64(-99),
|
||||
"temp_float": float64(-99.4),
|
||||
},
|
||||
metrics[0].Fields(),
|
||||
)
|
||||
assert.Equal(t,
|
||||
map[string]string{
|
||||
"host": "local",
|
||||
},
|
||||
metrics[0].Tags(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestParseErrors(t *testing.T) {
|
||||
start := time.Now()
|
||||
metrics, err := Parse([]byte(someInvalid))
|
||||
|
||||
155
metric/reader.go
Normal file
155
metric/reader.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package metric
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
)
|
||||
|
||||
type state int
|
||||
|
||||
const (
|
||||
_ state = iota
|
||||
// normal state copies whole metrics into the given buffer until we can't
|
||||
// fit the next metric.
|
||||
normal
|
||||
// split state means that we have a metric that we were able to split, so
|
||||
// that we can fit it into multiple metrics (and calls to Read)
|
||||
split
|
||||
// overflow state means that we have a metric that didn't fit into a single
|
||||
// buffer, and needs to be split across multiple calls to Read.
|
||||
overflow
|
||||
// splitOverflow state means that a split metric didn't fit into a single
|
||||
// buffer, and needs to be split across multiple calls to Read.
|
||||
splitOverflow
|
||||
// done means we're done reading metrics, and now always return (0, io.EOF)
|
||||
done
|
||||
)
|
||||
|
||||
type reader struct {
|
||||
metrics []telegraf.Metric
|
||||
splitMetrics []telegraf.Metric
|
||||
buf []byte
|
||||
state state
|
||||
|
||||
// metric index
|
||||
iM int
|
||||
// split metric index
|
||||
iSM int
|
||||
// buffer index
|
||||
iB int
|
||||
}
|
||||
|
||||
func NewReader(metrics []telegraf.Metric) io.Reader {
|
||||
return &reader{
|
||||
metrics: metrics,
|
||||
state: normal,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *reader) Read(p []byte) (n int, err error) {
|
||||
var i int
|
||||
switch r.state {
|
||||
case done:
|
||||
return 0, io.EOF
|
||||
case normal:
|
||||
for {
|
||||
// this for-loop is the sunny-day scenario, where we are given a
|
||||
// buffer that is large enough to hold at least a single metric.
|
||||
// all of the cases below it are edge-cases.
|
||||
if r.metrics[r.iM].Len() < len(p[i:]) {
|
||||
i += r.metrics[r.iM].SerializeTo(p[i:])
|
||||
} else {
|
||||
break
|
||||
}
|
||||
r.iM++
|
||||
if r.iM == len(r.metrics) {
|
||||
r.state = done
|
||||
return i, io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
// if we haven't written any bytes, check if we can split the current
|
||||
// metric into multiple full metrics at a smaller size.
|
||||
if i == 0 {
|
||||
tmp := r.metrics[r.iM].Split(len(p))
|
||||
if len(tmp) > 1 {
|
||||
r.splitMetrics = tmp
|
||||
r.state = split
|
||||
if r.splitMetrics[0].Len() < len(p) {
|
||||
i += r.splitMetrics[0].SerializeTo(p)
|
||||
r.iSM = 1
|
||||
} else {
|
||||
// splitting didn't quite work, so we'll drop down and
|
||||
// overflow the metric.
|
||||
r.state = normal
|
||||
r.iSM = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we haven't written any bytes and we're not at the end of the metrics
|
||||
// slice, then it means we have a single metric that is larger than the
|
||||
// provided buffer.
|
||||
if i == 0 {
|
||||
r.buf = r.metrics[r.iM].Serialize()
|
||||
i += copy(p, r.buf[r.iB:])
|
||||
r.iB += i
|
||||
r.state = overflow
|
||||
}
|
||||
|
||||
case split:
|
||||
if r.splitMetrics[r.iSM].Len() < len(p) {
|
||||
// write the current split metric
|
||||
i += r.splitMetrics[r.iSM].SerializeTo(p)
|
||||
r.iSM++
|
||||
if r.iSM >= len(r.splitMetrics) {
|
||||
// done writing the current split metrics
|
||||
r.iSM = 0
|
||||
r.iM++
|
||||
if r.iM == len(r.metrics) {
|
||||
r.state = done
|
||||
return i, io.EOF
|
||||
}
|
||||
r.state = normal
|
||||
}
|
||||
} else {
|
||||
// This would only happen if we split the metric, and then a
|
||||
// subsequent buffer was smaller than the initial one given,
|
||||
// so that our split metric no longer fits.
|
||||
r.buf = r.splitMetrics[r.iSM].Serialize()
|
||||
i += copy(p, r.buf[r.iB:])
|
||||
r.iB += i
|
||||
r.state = splitOverflow
|
||||
}
|
||||
|
||||
case splitOverflow:
|
||||
i = copy(p, r.buf[r.iB:])
|
||||
r.iB += i
|
||||
if r.iB >= len(r.buf) {
|
||||
r.iB = 0
|
||||
r.iSM++
|
||||
if r.iSM == len(r.splitMetrics) {
|
||||
r.iM++
|
||||
r.state = normal
|
||||
} else {
|
||||
r.state = split
|
||||
}
|
||||
}
|
||||
|
||||
case overflow:
|
||||
i = copy(p, r.buf[r.iB:])
|
||||
r.iB += i
|
||||
if r.iB >= len(r.buf) {
|
||||
r.iB = 0
|
||||
r.iM++
|
||||
if r.iM == len(r.metrics) {
|
||||
r.state = done
|
||||
return i, io.EOF
|
||||
}
|
||||
r.state = normal
|
||||
}
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
487
metric/reader_test.go
Normal file
487
metric/reader_test.go
Normal file
@@ -0,0 +1,487 @@
|
||||
package metric
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkMetricReader(b *testing.B) {
|
||||
metrics := make([]telegraf.Metric, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
metrics[i], _ = New("foo", map[string]string{},
|
||||
map[string]interface{}{"value": int64(1)}, time.Now())
|
||||
}
|
||||
for n := 0; n < b.N; n++ {
|
||||
r := NewReader(metrics)
|
||||
io.Copy(ioutil.Discard, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricReader(t *testing.T) {
|
||||
ts := time.Unix(1481032190, 0)
|
||||
metrics := make([]telegraf.Metric, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
metrics[i], _ = New("foo", map[string]string{},
|
||||
map[string]interface{}{"value": int64(1)}, ts)
|
||||
}
|
||||
|
||||
r := NewReader(metrics)
|
||||
|
||||
buf := make([]byte, 35)
|
||||
for i := 0; i < 10; i++ {
|
||||
n, err := r.Read(buf)
|
||||
if err != nil {
|
||||
assert.True(t, err == io.EOF, err.Error())
|
||||
}
|
||||
assert.Equal(t, 33, n)
|
||||
assert.Equal(t, "foo value=1i 1481032190000000000\n", string(buf[0:n]))
|
||||
}
|
||||
|
||||
// reader should now be done, and always return 0, io.EOF
|
||||
for i := 0; i < 10; i++ {
|
||||
n, err := r.Read(buf)
|
||||
assert.True(t, err == io.EOF, err.Error())
|
||||
assert.Equal(t, 0, n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricReader_OverflowMetric(t *testing.T) {
|
||||
ts := time.Unix(1481032190, 0)
|
||||
m, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{"value": int64(10)}, ts)
|
||||
metrics := []telegraf.Metric{m}
|
||||
|
||||
r := NewReader(metrics)
|
||||
buf := make([]byte, 5)
|
||||
|
||||
tests := []struct {
|
||||
exp string
|
||||
err error
|
||||
n int
|
||||
}{
|
||||
{
|
||||
"foo v",
|
||||
nil,
|
||||
5,
|
||||
},
|
||||
{
|
||||
"alue=",
|
||||
nil,
|
||||
5,
|
||||
},
|
||||
{
|
||||
"10i 1",
|
||||
nil,
|
||||
5,
|
||||
},
|
||||
{
|
||||
"48103",
|
||||
nil,
|
||||
5,
|
||||
},
|
||||
{
|
||||
"21900",
|
||||
nil,
|
||||
5,
|
||||
},
|
||||
{
|
||||
"00000",
|
||||
nil,
|
||||
5,
|
||||
},
|
||||
{
|
||||
"000\n",
|
||||
io.EOF,
|
||||
4,
|
||||
},
|
||||
{
|
||||
"",
|
||||
io.EOF,
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
n, err := r.Read(buf)
|
||||
assert.Equal(t, test.n, n)
|
||||
assert.Equal(t, test.exp, string(buf[0:n]))
|
||||
assert.Equal(t, test.err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricReader_OverflowMultipleMetrics(t *testing.T) {
|
||||
ts := time.Unix(1481032190, 0)
|
||||
m, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{"value": int64(10)}, ts)
|
||||
metrics := []telegraf.Metric{m, m.Copy()}
|
||||
|
||||
r := NewReader(metrics)
|
||||
buf := make([]byte, 10)
|
||||
|
||||
tests := []struct {
|
||||
exp string
|
||||
err error
|
||||
n int
|
||||
}{
|
||||
{
|
||||
"foo value=",
|
||||
nil,
|
||||
10,
|
||||
},
|
||||
{
|
||||
"10i 148103",
|
||||
nil,
|
||||
10,
|
||||
},
|
||||
{
|
||||
"2190000000",
|
||||
nil,
|
||||
10,
|
||||
},
|
||||
{
|
||||
"000\n",
|
||||
nil,
|
||||
4,
|
||||
},
|
||||
{
|
||||
"foo value=",
|
||||
nil,
|
||||
10,
|
||||
},
|
||||
{
|
||||
"10i 148103",
|
||||
nil,
|
||||
10,
|
||||
},
|
||||
{
|
||||
"2190000000",
|
||||
nil,
|
||||
10,
|
||||
},
|
||||
{
|
||||
"000\n",
|
||||
io.EOF,
|
||||
4,
|
||||
},
|
||||
{
|
||||
"",
|
||||
io.EOF,
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
n, err := r.Read(buf)
|
||||
assert.Equal(t, test.n, n)
|
||||
assert.Equal(t, test.exp, string(buf[0:n]))
|
||||
assert.Equal(t, test.err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// test splitting a metric
|
||||
func TestMetricReader_SplitMetric(t *testing.T) {
|
||||
ts := time.Unix(1481032190, 0)
|
||||
m1, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{
|
||||
"value1": int64(10),
|
||||
"value2": int64(10),
|
||||
"value3": int64(10),
|
||||
"value4": int64(10),
|
||||
"value5": int64(10),
|
||||
"value6": int64(10),
|
||||
},
|
||||
ts,
|
||||
)
|
||||
metrics := []telegraf.Metric{m1}
|
||||
|
||||
r := NewReader(metrics)
|
||||
buf := make([]byte, 60)
|
||||
|
||||
tests := []struct {
|
||||
expRegex string
|
||||
err error
|
||||
n int
|
||||
}{
|
||||
{
|
||||
`foo value\d=10i,value\d=10i,value\d=10i 1481032190000000000\n`,
|
||||
nil,
|
||||
57,
|
||||
},
|
||||
{
|
||||
`foo value\d=10i,value\d=10i,value\d=10i 1481032190000000000\n`,
|
||||
io.EOF,
|
||||
57,
|
||||
},
|
||||
{
|
||||
"",
|
||||
io.EOF,
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
n, err := r.Read(buf)
|
||||
assert.Equal(t, test.n, n)
|
||||
re := regexp.MustCompile(test.expRegex)
|
||||
assert.True(t, re.MatchString(string(buf[0:n])), string(buf[0:n]))
|
||||
assert.Equal(t, test.err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// test an array with one split metric and one unsplit
|
||||
func TestMetricReader_SplitMetric2(t *testing.T) {
|
||||
ts := time.Unix(1481032190, 0)
|
||||
m1, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{
|
||||
"value1": int64(10),
|
||||
"value2": int64(10),
|
||||
"value3": int64(10),
|
||||
"value4": int64(10),
|
||||
"value5": int64(10),
|
||||
"value6": int64(10),
|
||||
},
|
||||
ts,
|
||||
)
|
||||
m2, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{
|
||||
"value1": int64(10),
|
||||
},
|
||||
ts,
|
||||
)
|
||||
metrics := []telegraf.Metric{m1, m2}
|
||||
|
||||
r := NewReader(metrics)
|
||||
buf := make([]byte, 60)
|
||||
|
||||
tests := []struct {
|
||||
expRegex string
|
||||
err error
|
||||
n int
|
||||
}{
|
||||
{
|
||||
`foo value\d=10i,value\d=10i,value\d=10i 1481032190000000000\n`,
|
||||
nil,
|
||||
57,
|
||||
},
|
||||
{
|
||||
`foo value\d=10i,value\d=10i,value\d=10i 1481032190000000000\n`,
|
||||
nil,
|
||||
57,
|
||||
},
|
||||
{
|
||||
`foo value1=10i 1481032190000000000\n`,
|
||||
io.EOF,
|
||||
35,
|
||||
},
|
||||
{
|
||||
"",
|
||||
io.EOF,
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
n, err := r.Read(buf)
|
||||
assert.Equal(t, test.n, n)
|
||||
re := regexp.MustCompile(test.expRegex)
|
||||
assert.True(t, re.MatchString(string(buf[0:n])), string(buf[0:n]))
|
||||
assert.Equal(t, test.err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// test split that results in metrics that are still too long, which results in
|
||||
// the reader falling back to regular overflow.
|
||||
func TestMetricReader_SplitMetricTooLong(t *testing.T) {
|
||||
ts := time.Unix(1481032190, 0)
|
||||
m1, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{
|
||||
"value1": int64(10),
|
||||
"value2": int64(10),
|
||||
},
|
||||
ts,
|
||||
)
|
||||
metrics := []telegraf.Metric{m1}
|
||||
|
||||
r := NewReader(metrics)
|
||||
buf := make([]byte, 30)
|
||||
|
||||
tests := []struct {
|
||||
expRegex string
|
||||
err error
|
||||
n int
|
||||
}{
|
||||
{
|
||||
`foo value\d=10i,value\d=10i 1481`,
|
||||
nil,
|
||||
30,
|
||||
},
|
||||
{
|
||||
`032190000000000\n`,
|
||||
io.EOF,
|
||||
16,
|
||||
},
|
||||
{
|
||||
"",
|
||||
io.EOF,
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
n, err := r.Read(buf)
|
||||
assert.Equal(t, test.n, n)
|
||||
re := regexp.MustCompile(test.expRegex)
|
||||
assert.True(t, re.MatchString(string(buf[0:n])), string(buf[0:n]))
|
||||
assert.Equal(t, test.err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// test split with a changing buffer size in the middle of subsequent calls
|
||||
// to Read
|
||||
func TestMetricReader_SplitMetricChangingBuffer(t *testing.T) {
|
||||
ts := time.Unix(1481032190, 0)
|
||||
m1, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{
|
||||
"value1": int64(10),
|
||||
"value2": int64(10),
|
||||
"value3": int64(10),
|
||||
},
|
||||
ts,
|
||||
)
|
||||
m2, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{
|
||||
"value1": int64(10),
|
||||
},
|
||||
ts,
|
||||
)
|
||||
metrics := []telegraf.Metric{m1, m2}
|
||||
|
||||
r := NewReader(metrics)
|
||||
|
||||
tests := []struct {
|
||||
expRegex string
|
||||
err error
|
||||
n int
|
||||
buf []byte
|
||||
}{
|
||||
{
|
||||
`foo value\d=10i 1481032190000000000\n`,
|
||||
nil,
|
||||
35,
|
||||
make([]byte, 36),
|
||||
},
|
||||
{
|
||||
`foo value\d=10i 148103219000000`,
|
||||
nil,
|
||||
30,
|
||||
make([]byte, 30),
|
||||
},
|
||||
{
|
||||
`0000\n`,
|
||||
nil,
|
||||
5,
|
||||
make([]byte, 30),
|
||||
},
|
||||
{
|
||||
`foo value\d=10i 1481032190000000000\n`,
|
||||
nil,
|
||||
35,
|
||||
make([]byte, 36),
|
||||
},
|
||||
{
|
||||
`foo value1=10i 1481032190000000000\n`,
|
||||
io.EOF,
|
||||
35,
|
||||
make([]byte, 36),
|
||||
},
|
||||
{
|
||||
"",
|
||||
io.EOF,
|
||||
0,
|
||||
make([]byte, 36),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
n, err := r.Read(test.buf)
|
||||
assert.Equal(t, test.n, n, test.expRegex)
|
||||
re := regexp.MustCompile(test.expRegex)
|
||||
assert.True(t, re.MatchString(string(test.buf[0:n])), string(test.buf[0:n]))
|
||||
assert.Equal(t, test.err, err, test.expRegex)
|
||||
}
|
||||
}
|
||||
|
||||
// test split with a changing buffer size in the middle of subsequent calls
|
||||
// to Read
|
||||
func TestMetricReader_SplitMetricChangingBuffer2(t *testing.T) {
|
||||
ts := time.Unix(1481032190, 0)
|
||||
m1, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{
|
||||
"value1": int64(10),
|
||||
"value2": int64(10),
|
||||
},
|
||||
ts,
|
||||
)
|
||||
m2, _ := New("foo", map[string]string{},
|
||||
map[string]interface{}{
|
||||
"value1": int64(10),
|
||||
},
|
||||
ts,
|
||||
)
|
||||
metrics := []telegraf.Metric{m1, m2}
|
||||
|
||||
r := NewReader(metrics)
|
||||
|
||||
tests := []struct {
|
||||
expRegex string
|
||||
err error
|
||||
n int
|
||||
buf []byte
|
||||
}{
|
||||
{
|
||||
`foo value\d=10i 1481032190000000000\n`,
|
||||
nil,
|
||||
35,
|
||||
make([]byte, 36),
|
||||
},
|
||||
{
|
||||
`foo value\d=10i 148103219000000`,
|
||||
nil,
|
||||
30,
|
||||
make([]byte, 30),
|
||||
},
|
||||
{
|
||||
`0000\n`,
|
||||
nil,
|
||||
5,
|
||||
make([]byte, 30),
|
||||
},
|
||||
{
|
||||
`foo value1=10i 1481032190000000000\n`,
|
||||
io.EOF,
|
||||
35,
|
||||
make([]byte, 36),
|
||||
},
|
||||
{
|
||||
"",
|
||||
io.EOF,
|
||||
0,
|
||||
make([]byte, 36),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
n, err := r.Read(test.buf)
|
||||
assert.Equal(t, test.n, n, test.expRegex)
|
||||
re := regexp.MustCompile(test.expRegex)
|
||||
assert.True(t, re.MatchString(string(test.buf[0:n])), string(test.buf[0:n]))
|
||||
assert.Equal(t, test.err, err, test.expRegex)
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,7 @@ import (
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/sensors"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/snmp"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/snmp_legacy"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/socket_listener"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/sqlserver"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/statsd"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/sysstat"
|
||||
|
||||
@@ -117,7 +117,7 @@ All fields are collected under the **ceph** measurement and stored as float64s.
|
||||
* recovering\_objects\_per\_sec (float)
|
||||
|
||||
* ceph\_pgmap\_state
|
||||
* state name e.g. active+clean (float)
|
||||
* count (float)
|
||||
|
||||
* ceph\_usage
|
||||
* bytes\_used (float)
|
||||
@@ -186,7 +186,7 @@ All measurements will have the following tags:
|
||||
|
||||
*Cluster Stats*
|
||||
|
||||
* ceph\_pg\_state has the following tags:
|
||||
* ceph\_pgmap\_state has the following tags:
|
||||
* state (state for which the value applies e.g. active+clean, active+remapped+backfill)
|
||||
* ceph\_pool\_usage has the following tags:
|
||||
* id
|
||||
@@ -213,7 +213,8 @@ telegraf -test -config /etc/telegraf/telegraf.conf -config-directory /etc/telegr
|
||||
<pre>
|
||||
> ceph_osdmap,host=ceph-mon-0 epoch=170772,full=false,nearfull=false,num_in_osds=340,num_osds=340,num_remapped_pgs=0,num_up_osds=340 1468841037000000000
|
||||
> ceph_pgmap,host=ceph-mon-0 bytes_avail=634895531270144,bytes_total=812117151809536,bytes_used=177221620539392,data_bytes=56979991615058,num_pgs=22952,op_per_sec=15869,read_bytes_sec=43956026,version=39387592,write_bytes_sec=165344818 1468841037000000000
|
||||
> ceph_pgmap_state,host=ceph-mon-0 active+clean=22952 1468928660000000000
|
||||
> ceph_pgmap_state,host=ceph-mon-0,state=active+clean count=22952 1468928660000000000
|
||||
> ceph_pgmap_state,host=ceph-mon-0,state=active+degraded count=16 1468928660000000000
|
||||
> ceph_usage,host=ceph-mon-0 total_avail_bytes=634895514791936,total_bytes=812117151809536,total_used_bytes=177221637017600 1468841037000000000
|
||||
> ceph_pool_usage,host=ceph-mon-0,id=150,name=cinder.volumes bytes_used=12648553794802,kb_used=12352103316,max_avail=154342562489244,objects=3026295 1468841037000000000
|
||||
> ceph_pool_usage,host=ceph-mon-0,id=182,name=cinder.volumes.flash bytes_used=8541308223964,kb_used=8341121313,max_avail=39388593563936,objects=2075066 1468841037000000000
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -108,7 +109,7 @@ func (c *Ceph) gatherAdminSocketStats(acc telegraf.Accumulator) error {
|
||||
log.Printf("E! error parsing dump from socket '%s': %v", s.socket, err)
|
||||
continue
|
||||
}
|
||||
for tag, metrics := range *data {
|
||||
for tag, metrics := range data {
|
||||
acc.AddFields(measurement,
|
||||
map[string]interface{}(metrics),
|
||||
map[string]string{"type": s.sockType, "id": s.sockId, "collection": tag})
|
||||
@@ -244,25 +245,19 @@ type taggedMetricMap map[string]metricMap
|
||||
|
||||
// Parses a raw JSON string into a taggedMetricMap
|
||||
// Delegates the actual parsing to newTaggedMetricMap(..)
|
||||
func parseDump(dump string) (*taggedMetricMap, error) {
|
||||
func parseDump(dump string) (taggedMetricMap, error) {
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(dump), &data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse json: '%s': %v", dump, err)
|
||||
}
|
||||
|
||||
tmm := newTaggedMetricMap(data)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to tag dataset: '%v': %v", tmm, err)
|
||||
}
|
||||
|
||||
return tmm, nil
|
||||
return newTaggedMetricMap(data), nil
|
||||
}
|
||||
|
||||
// Builds a TaggedMetricMap out of a generic string map.
|
||||
// The top-level key is used as a tag and all sub-keys are flattened into metrics
|
||||
func newTaggedMetricMap(data map[string]interface{}) *taggedMetricMap {
|
||||
func newTaggedMetricMap(data map[string]interface{}) taggedMetricMap {
|
||||
tmm := make(taggedMetricMap)
|
||||
for tag, datapoints := range data {
|
||||
mm := make(metricMap)
|
||||
@@ -271,7 +266,7 @@ func newTaggedMetricMap(data map[string]interface{}) *taggedMetricMap {
|
||||
}
|
||||
tmm[tag] = mm
|
||||
}
|
||||
return &tmm
|
||||
return tmm
|
||||
}
|
||||
|
||||
// Recursively flattens any k-v hierarchy present in data.
|
||||
@@ -376,36 +371,53 @@ func decodeStatusPgmap(acc telegraf.Accumulator, data map[string]interface{}) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeStatusPgmapState(acc telegraf.Accumulator, data map[string]interface{}) error {
|
||||
func extractPgmapStates(data map[string]interface{}) ([]interface{}, error) {
|
||||
const key = "pgs_by_state"
|
||||
|
||||
pgmap, ok := data["pgmap"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("WARNING %s - unable to decode pgmap", measurement)
|
||||
return nil, fmt.Errorf("WARNING %s - unable to decode pgmap", measurement)
|
||||
}
|
||||
fields := make(map[string]interface{})
|
||||
for key, value := range pgmap {
|
||||
switch value.(type) {
|
||||
case []interface{}:
|
||||
if key != "pgs_by_state" {
|
||||
continue
|
||||
}
|
||||
for _, state := range value.([]interface{}) {
|
||||
state_map, ok := state.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("WARNING %s - unable to decode pg state", measurement)
|
||||
}
|
||||
state_name, ok := state_map["state_name"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("WARNING %s - unable to decode pg state name", measurement)
|
||||
}
|
||||
state_count, ok := state_map["count"].(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("WARNING %s - unable to decode pg state count", measurement)
|
||||
}
|
||||
fields[state_name] = state_count
|
||||
}
|
||||
|
||||
s, ok := pgmap[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("WARNING %s - pgmap is missing the %s field", measurement, key)
|
||||
}
|
||||
|
||||
states, ok := s.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("WARNING %s - pgmap[%s] is not a list", measurement, key)
|
||||
}
|
||||
return states, nil
|
||||
}
|
||||
|
||||
func decodeStatusPgmapState(acc telegraf.Accumulator, data map[string]interface{}) error {
|
||||
states, err := extractPgmapStates(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, state := range states {
|
||||
stateMap, ok := state.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("WARNING %s - unable to decode pg state", measurement)
|
||||
}
|
||||
stateName, ok := stateMap["state_name"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("WARNING %s - unable to decode pg state name", measurement)
|
||||
}
|
||||
stateCount, ok := stateMap["count"].(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("WARNING %s - unable to decode pg state count", measurement)
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"state": stateName,
|
||||
}
|
||||
fields := map[string]interface{}{
|
||||
"count": stateCount,
|
||||
}
|
||||
acc.AddFields("ceph_pgmap_state", fields, tags)
|
||||
}
|
||||
acc.AddFields("ceph_pgmap_state", fields, map[string]string{})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package ceph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,15 +26,38 @@ func TestParseSockId(t *testing.T) {
|
||||
func TestParseMonDump(t *testing.T) {
|
||||
dump, err := parseDump(monPerfDump)
|
||||
assert.NoError(t, err)
|
||||
assert.InEpsilon(t, 5678670180, (*dump)["cluster"]["osd_kb_used"], epsilon)
|
||||
assert.InEpsilon(t, 6866.540527000, (*dump)["paxos"]["store_state_latency.sum"], epsilon)
|
||||
assert.InEpsilon(t, 5678670180, dump["cluster"]["osd_kb_used"], epsilon)
|
||||
assert.InEpsilon(t, 6866.540527000, dump["paxos"]["store_state_latency.sum"], epsilon)
|
||||
}
|
||||
|
||||
func TestParseOsdDump(t *testing.T) {
|
||||
dump, err := parseDump(osdPerfDump)
|
||||
assert.NoError(t, err)
|
||||
assert.InEpsilon(t, 552132.109360000, (*dump)["filestore"]["commitcycle_interval.sum"], epsilon)
|
||||
assert.Equal(t, float64(0), (*dump)["mutex-FileJournal::finisher_lock"]["wait.avgcount"])
|
||||
assert.InEpsilon(t, 552132.109360000, dump["filestore"]["commitcycle_interval.sum"], epsilon)
|
||||
assert.Equal(t, float64(0), dump["mutex-FileJournal::finisher_lock"]["wait.avgcount"])
|
||||
}
|
||||
|
||||
func TestDecodeStatusPgmapState(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(clusterStatusDump), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
acc := &testutil.Accumulator{}
|
||||
err = decodeStatusPgmapState(acc, data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var results = []struct {
|
||||
fields map[string]interface{}
|
||||
tags map[string]string
|
||||
}{
|
||||
{map[string]interface{}{"count": float64(2560)}, map[string]string{"state": "active+clean"}},
|
||||
{map[string]interface{}{"count": float64(10)}, map[string]string{"state": "active+scrubbing"}},
|
||||
{map[string]interface{}{"count": float64(5)}, map[string]string{"state": "active+backfilling"}},
|
||||
}
|
||||
|
||||
for _, r := range results {
|
||||
acc.AssertContainsTaggedFields(t, "ceph_pgmap_state", r.fields, r.tags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGather(t *testing.T) {
|
||||
@@ -685,3 +710,127 @@ var osdPerfDump = `
|
||||
"wait": { "avgcount": 0,
|
||||
"sum": 0.000000000}}}
|
||||
`
|
||||
var clusterStatusDump = `
|
||||
{
|
||||
"health": {
|
||||
"health": {
|
||||
"health_services": [
|
||||
{
|
||||
"mons": [
|
||||
{
|
||||
"name": "a",
|
||||
"kb_total": 114289256,
|
||||
"kb_used": 26995516,
|
||||
"kb_avail": 81465132,
|
||||
"avail_percent": 71,
|
||||
"last_updated": "2017-01-03 17:20:57.595004",
|
||||
"store_stats": {
|
||||
"bytes_total": 942117141,
|
||||
"bytes_sst": 0,
|
||||
"bytes_log": 4345406,
|
||||
"bytes_misc": 937771735,
|
||||
"last_updated": "0.000000"
|
||||
},
|
||||
"health": "HEALTH_OK"
|
||||
},
|
||||
{
|
||||
"name": "b",
|
||||
"kb_total": 114289256,
|
||||
"kb_used": 27871624,
|
||||
"kb_avail": 80589024,
|
||||
"avail_percent": 70,
|
||||
"last_updated": "2017-01-03 17:20:47.784331",
|
||||
"store_stats": {
|
||||
"bytes_total": 454853104,
|
||||
"bytes_sst": 0,
|
||||
"bytes_log": 5788320,
|
||||
"bytes_misc": 449064784,
|
||||
"last_updated": "0.000000"
|
||||
},
|
||||
"health": "HEALTH_OK"
|
||||
},
|
||||
{
|
||||
"name": "c",
|
||||
"kb_total": 130258508,
|
||||
"kb_used": 38076996,
|
||||
"kb_avail": 85541692,
|
||||
"avail_percent": 65,
|
||||
"last_updated": "2017-01-03 17:21:03.311123",
|
||||
"store_stats": {
|
||||
"bytes_total": 455555199,
|
||||
"bytes_sst": 0,
|
||||
"bytes_log": 6950876,
|
||||
"bytes_misc": 448604323,
|
||||
"last_updated": "0.000000"
|
||||
},
|
||||
"health": "HEALTH_OK"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"timechecks": {
|
||||
"epoch": 504,
|
||||
"round": 34642,
|
||||
"round_status": "finished",
|
||||
"mons": [
|
||||
{ "name": "a", "skew": 0, "latency": 0, "health": "HEALTH_OK" },
|
||||
{ "name": "b", "skew": -0, "latency": 0.000951, "health": "HEALTH_OK" },
|
||||
{ "name": "c", "skew": -0, "latency": 0.000946, "health": "HEALTH_OK" }
|
||||
]
|
||||
},
|
||||
"summary": [],
|
||||
"overall_status": "HEALTH_OK",
|
||||
"detail": []
|
||||
},
|
||||
"fsid": "01234567-abcd-9876-0123-ffeeddccbbaa",
|
||||
"election_epoch": 504,
|
||||
"quorum": [ 0, 1, 2 ],
|
||||
"quorum_names": [ "a", "b", "c" ],
|
||||
"monmap": {
|
||||
"epoch": 17,
|
||||
"fsid": "01234567-abcd-9876-0123-ffeeddccbbaa",
|
||||
"modified": "2016-04-11 14:01:52.600198",
|
||||
"created": "0.000000",
|
||||
"mons": [
|
||||
{ "rank": 0, "name": "a", "addr": "192.168.0.1:6789/0" },
|
||||
{ "rank": 1, "name": "b", "addr": "192.168.0.2:6789/0" },
|
||||
{ "rank": 2, "name": "c", "addr": "192.168.0.3:6789/0" }
|
||||
]
|
||||
},
|
||||
"osdmap": {
|
||||
"osdmap": {
|
||||
"epoch": 21734,
|
||||
"num_osds": 24,
|
||||
"num_up_osds": 24,
|
||||
"num_in_osds": 24,
|
||||
"full": false,
|
||||
"nearfull": false,
|
||||
"num_remapped_pgs": 0
|
||||
}
|
||||
},
|
||||
"pgmap": {
|
||||
"pgs_by_state": [
|
||||
{ "state_name": "active+clean", "count": 2560 },
|
||||
{ "state_name": "active+scrubbing", "count": 10 },
|
||||
{ "state_name": "active+backfilling", "count": 5 }
|
||||
],
|
||||
"version": 52314277,
|
||||
"num_pgs": 2560,
|
||||
"data_bytes": 2700031960713,
|
||||
"bytes_used": 7478347665408,
|
||||
"bytes_avail": 9857462382592,
|
||||
"bytes_total": 17335810048000,
|
||||
"read_bytes_sec": 0,
|
||||
"write_bytes_sec": 367217,
|
||||
"op_per_sec": 98
|
||||
},
|
||||
"mdsmap": {
|
||||
"epoch": 1,
|
||||
"up": 0,
|
||||
"in": 0,
|
||||
"max": 0,
|
||||
"by_rank": []
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -35,12 +35,19 @@ Fields:
|
||||
- check_name
|
||||
- service_id
|
||||
- status
|
||||
- passing
|
||||
- critical
|
||||
- warning
|
||||
|
||||
`passing`, `critical`, and `warning` are integer representations of the health
|
||||
check state. A value of `1` represents that the status was the state of the
|
||||
the health check at this sample.
|
||||
|
||||
## Example output
|
||||
|
||||
```
|
||||
$ telegraf --config ./telegraf.conf -input-filter consul -test
|
||||
* Plugin: consul, Collection 1
|
||||
> consul_health_checks,host=wolfpit,node=consul-server-node,check_id="serfHealth" check_name="Serf Health Status",service_id="",status="passing" 1464698464486439902
|
||||
> consul_health_checks,host=wolfpit,node=consul-server-node,service_name=www.example.com,check_id="service:www-example-com.test01" check_name="Service 'www.example.com' check",service_id="www-example-com.test01",status="critical" 1464698464486519036
|
||||
> consul_health_checks,host=wolfpit,node=consul-server-node,check_id="serfHealth" check_name="Serf Health Status",service_id="",status="passing",passing=1i,critical=0i,warning=0i 1464698464486439902
|
||||
> consul_health_checks,host=wolfpit,node=consul-server-node,service_name=www.example.com,check_id="service:www-example-com.test01" check_name="Service 'www.example.com' check",service_id="www-example-com.test01",status="critical",passing=0i,critical=1i,warning=0i 1464698464486519036
|
||||
```
|
||||
|
||||
@@ -97,7 +97,12 @@ func (c *Consul) GatherHealthCheck(acc telegraf.Accumulator, checks []*api.Healt
|
||||
|
||||
record["check_name"] = check.Name
|
||||
record["service_id"] = check.ServiceID
|
||||
|
||||
record["status"] = check.Status
|
||||
record["passing"] = 0
|
||||
record["critical"] = 0
|
||||
record["warning"] = 0
|
||||
record[check.Status] = 1
|
||||
|
||||
tags["node"] = check.Node
|
||||
tags["service_name"] = check.ServiceName
|
||||
|
||||
@@ -24,6 +24,9 @@ func TestGatherHealtCheck(t *testing.T) {
|
||||
expectedFields := map[string]interface{}{
|
||||
"check_name": "foo.health",
|
||||
"status": "passing",
|
||||
"passing": 1,
|
||||
"critical": 0,
|
||||
"warning": 0,
|
||||
"service_id": "foo.123",
|
||||
}
|
||||
|
||||
|
||||
@@ -273,7 +273,6 @@ func (d FakeDockerClient) Info(ctx context.Context) (types.Info, error) {
|
||||
Name: "absol",
|
||||
SwapLimit: false,
|
||||
IPv4Forwarding: true,
|
||||
ExecutionDriver: "native-0.2",
|
||||
ExperimentalBuild: false,
|
||||
CPUCfsPeriod: true,
|
||||
RegistryConfig: ®istry.ServiceConfig{
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
servers = ["http://1.2.3.4/haproxy?stats", "/var/run/haproxy*.sock"]
|
||||
```
|
||||
|
||||
#### `servers`
|
||||
Server addresses need to explicitly start with 'http' if you wish to use HAproxy status page. Otherwise, address will be assumed to be an UNIX socket and protocol (if present) will be discarded.
|
||||
|
||||
For basic authentication you need to add username and password in the URL: `http://user:password@1.2.3.4/haproxy?stats`.
|
||||
@@ -26,9 +27,12 @@ When using socket names, wildcard expansion is supported so plugin can gather st
|
||||
|
||||
If no servers are specified, then the default address of `http://127.0.0.1:1936/haproxy?stats` will be used.
|
||||
|
||||
#### `keep_field_names`
|
||||
By default, some of the fields are renamed from what haproxy calls them. Setting the `keep_field_names` parameter to `true` will result in the plugin keeping the original field names.
|
||||
|
||||
### Measurements & Fields:
|
||||
|
||||
Plugin will gather measurements outlined in [HAproxy CSV format documentation](https://cbonte.github.io/haproxy-dconv/1.5/configuration.html#9.1).
|
||||
Plugin will gather measurements outlined in [HAproxy CSV format documentation](https://cbonte.github.io/haproxy-dconv/1.7/management.html#9.1).
|
||||
|
||||
### Tags:
|
||||
|
||||
|
||||
@@ -14,80 +14,17 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/internal/errchan"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
//CSV format: https://cbonte.github.io/haproxy-dconv/1.5/configuration.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
|
||||
|
||||
KeepFieldNames bool
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
@@ -103,6 +40,11 @@ var sampleConfig = `
|
||||
## Server address not starting with 'http' will be treated as a possible
|
||||
## socket, so both examples below are valid.
|
||||
## servers = ["socket:/run/haproxy/admin.sock", "/run/haproxy/*.sock"]
|
||||
#
|
||||
## By default, some of the fields are renamed from what haproxy calls them.
|
||||
## Setting this option to true results in the plugin keeping the original
|
||||
## field names.
|
||||
## keep_field_names = true
|
||||
`
|
||||
|
||||
func (r *haproxy) SampleConfig() string {
|
||||
@@ -147,17 +89,18 @@ func (g *haproxy) Gather(acc telegraf.Accumulator) error {
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errChan := errchan.New(len(endpoints))
|
||||
wg.Add(len(endpoints))
|
||||
for _, server := range endpoints {
|
||||
go func(serv string) {
|
||||
defer wg.Done()
|
||||
errChan.C <- g.gatherServer(serv, acc)
|
||||
if err := g.gatherServer(serv, acc); err != nil {
|
||||
acc.AddError(err)
|
||||
}
|
||||
}(server)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return errChan.Error()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *haproxy) gatherServerSocket(addr string, acc telegraf.Accumulator) error {
|
||||
@@ -175,7 +118,7 @@ func (g *haproxy) gatherServerSocket(addr string, acc telegraf.Accumulator) erro
|
||||
return fmt.Errorf("Could not write to socket '%s': %s", addr, errw)
|
||||
}
|
||||
|
||||
return importCsvResult(c, acc, socketPath)
|
||||
return g.importCsvResult(c, acc, socketPath)
|
||||
}
|
||||
|
||||
func (g *haproxy) gatherServer(addr string, acc telegraf.Accumulator) error {
|
||||
@@ -216,7 +159,11 @@ func (g *haproxy) gatherServer(addr string, acc telegraf.Accumulator) error {
|
||||
return fmt.Errorf("Unable to get valid stat result from '%s', http response code : %d", addr, res.StatusCode)
|
||||
}
|
||||
|
||||
return importCsvResult(res.Body, acc, u.Host)
|
||||
if err := g.importCsvResult(res.Body, acc, u.Host); err != nil {
|
||||
return fmt.Errorf("Unable to parse stat result from '%s': %s", addr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSocketAddr(sock string) string {
|
||||
@@ -229,205 +176,96 @@ func getSocketAddr(sock string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func importCsvResult(r io.Reader, acc telegraf.Accumulator, host string) error {
|
||||
csv := csv.NewReader(r)
|
||||
result, err := csv.ReadAll()
|
||||
var typeNames = []string{"frontend", "backend", "server", "listener"}
|
||||
var fieldRenames = map[string]string{
|
||||
"pxname": "proxy",
|
||||
"svname": "sv",
|
||||
"act": "active_servers",
|
||||
"bck": "backup_servers",
|
||||
"cli_abrt": "cli_abort",
|
||||
"srv_abrt": "srv_abort",
|
||||
"hrsp_1xx": "http_response.1xx",
|
||||
"hrsp_2xx": "http_response.2xx",
|
||||
"hrsp_3xx": "http_response.3xx",
|
||||
"hrsp_4xx": "http_response.4xx",
|
||||
"hrsp_5xx": "http_response.5xx",
|
||||
"hrsp_other": "http_response.other",
|
||||
}
|
||||
|
||||
func (g *haproxy) importCsvResult(r io.Reader, acc telegraf.Accumulator, host string) error {
|
||||
csvr := csv.NewReader(r)
|
||||
now := time.Now()
|
||||
|
||||
for _, row := range result {
|
||||
headers, err := csvr.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(headers[0]) <= 2 || headers[0][:2] != "# " {
|
||||
return fmt.Errorf("did not receive standard haproxy headers")
|
||||
}
|
||||
headers[0] = headers[0][2:]
|
||||
|
||||
for {
|
||||
row, err := csvr.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if len(row) != len(headers) {
|
||||
return fmt.Errorf("number of columns does not match number of headers. headers=%d columns=%d", len(headers), len(row))
|
||||
}
|
||||
for i, v := range row {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
colName := headers[i]
|
||||
fieldName := colName
|
||||
if !g.KeepFieldNames {
|
||||
if fieldRename, ok := fieldRenames[colName]; ok {
|
||||
fieldName = fieldRename
|
||||
}
|
||||
case HF_QMAX:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["qmax"] = ival
|
||||
}
|
||||
|
||||
switch colName {
|
||||
case "pxname", "svname":
|
||||
tags[fieldName] = v
|
||||
case "type":
|
||||
vi, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse type value '%s'", v)
|
||||
}
|
||||
case HF_SCUR:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["scur"] = ival
|
||||
if int(vi) >= len(typeNames) {
|
||||
return fmt.Errorf("received unknown type value: %d", vi)
|
||||
}
|
||||
case HF_SMAX:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["smax"] = ival
|
||||
tags[fieldName] = typeNames[vi]
|
||||
case "check_desc", "agent_desc":
|
||||
// do nothing. These fields are just a more verbose description of the check_status & agent_status fields
|
||||
case "status", "check_status", "last_chk", "mode", "tracked", "agent_status", "last_agt", "addr", "cookie":
|
||||
// these are string fields
|
||||
fields[fieldName] = v
|
||||
case "lastsess":
|
||||
vi, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
//TODO log the error. And just once (per column) so we don't spam the log
|
||||
continue
|
||||
}
|
||||
case HF_SLIM:
|
||||
ival, err := strconv.ParseUint(v, 10, 64)
|
||||
if err == nil {
|
||||
fields["slim"] = 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
|
||||
fields[fieldName] = vi
|
||||
default:
|
||||
vi, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
//TODO log the error. And just once (per column) so we don't spam the log
|
||||
continue
|
||||
}
|
||||
fields[fieldName] = vi
|
||||
}
|
||||
}
|
||||
acc.AddFields("haproxy", fields, tags, now)
|
||||
|
||||
@@ -68,8 +68,9 @@ func TestHaproxyGeneratesMetricsWithAuthentication(t *testing.T) {
|
||||
|
||||
tags := map[string]string{
|
||||
"server": ts.Listener.Addr().String(),
|
||||
"proxy": "be_app",
|
||||
"sv": "host0",
|
||||
"proxy": "git",
|
||||
"sv": "www",
|
||||
"type": "server",
|
||||
}
|
||||
|
||||
fields := HaproxyGetFieldValues()
|
||||
@@ -80,8 +81,8 @@ func TestHaproxyGeneratesMetricsWithAuthentication(t *testing.T) {
|
||||
Servers: []string{ts.URL},
|
||||
}
|
||||
|
||||
err = r.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
r.Gather(&acc)
|
||||
require.NotEmpty(t, acc.Errors)
|
||||
}
|
||||
|
||||
func TestHaproxyGeneratesMetricsWithoutAuthentication(t *testing.T) {
|
||||
@@ -100,9 +101,10 @@ func TestHaproxyGeneratesMetricsWithoutAuthentication(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
tags := map[string]string{
|
||||
"proxy": "be_app",
|
||||
"server": ts.Listener.Addr().String(),
|
||||
"sv": "host0",
|
||||
"proxy": "git",
|
||||
"sv": "www",
|
||||
"type": "server",
|
||||
}
|
||||
|
||||
fields := HaproxyGetFieldValues()
|
||||
@@ -144,9 +146,10 @@ func TestHaproxyGeneratesMetricsUsingSocket(t *testing.T) {
|
||||
|
||||
for _, sock := range sockets {
|
||||
tags := map[string]string{
|
||||
"proxy": "be_app",
|
||||
"server": sock.Addr().String(),
|
||||
"sv": "host0",
|
||||
"proxy": "git",
|
||||
"sv": "www",
|
||||
"type": "server",
|
||||
}
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "haproxy", fields, tags)
|
||||
@@ -155,8 +158,8 @@ func TestHaproxyGeneratesMetricsUsingSocket(t *testing.T) {
|
||||
// This mask should not match any socket
|
||||
r.Servers = []string{_badmask}
|
||||
|
||||
err = r.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
r.Gather(&acc)
|
||||
require.NotEmpty(t, acc.Errors)
|
||||
}
|
||||
|
||||
//When not passing server config, we default to localhost
|
||||
@@ -171,59 +174,122 @@ func TestHaproxyDefaultGetFromLocalhost(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "127.0.0.1:1936/haproxy?stats/;csv")
|
||||
}
|
||||
|
||||
func TestHaproxyKeepFieldNames(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},
|
||||
KeepFieldNames: true,
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := r.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags := map[string]string{
|
||||
"server": ts.Listener.Addr().String(),
|
||||
"pxname": "git",
|
||||
"svname": "www",
|
||||
"type": "server",
|
||||
}
|
||||
|
||||
fields := HaproxyGetFieldValues()
|
||||
fields["act"] = fields["active_servers"]
|
||||
delete(fields, "active_servers")
|
||||
fields["bck"] = fields["backup_servers"]
|
||||
delete(fields, "backup_servers")
|
||||
fields["cli_abrt"] = fields["cli_abort"]
|
||||
delete(fields, "cli_abort")
|
||||
fields["srv_abrt"] = fields["srv_abort"]
|
||||
delete(fields, "srv_abort")
|
||||
fields["hrsp_1xx"] = fields["http_response.1xx"]
|
||||
delete(fields, "http_response.1xx")
|
||||
fields["hrsp_2xx"] = fields["http_response.2xx"]
|
||||
delete(fields, "http_response.2xx")
|
||||
fields["hrsp_3xx"] = fields["http_response.3xx"]
|
||||
delete(fields, "http_response.3xx")
|
||||
fields["hrsp_4xx"] = fields["http_response.4xx"]
|
||||
delete(fields, "http_response.4xx")
|
||||
fields["hrsp_5xx"] = fields["http_response.5xx"]
|
||||
delete(fields, "http_response.5xx")
|
||||
fields["hrsp_other"] = fields["http_response.other"]
|
||||
delete(fields, "http_response.other")
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "haproxy", fields, tags)
|
||||
}
|
||||
|
||||
func HaproxyGetFieldValues() map[string]interface{} {
|
||||
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),
|
||||
"slim": uint64(32),
|
||||
"srv_abort": uint64(1),
|
||||
"stot": uint64(171014),
|
||||
"ttime": uint64(2341),
|
||||
"wredis": uint64(0),
|
||||
"wretr": uint64(1),
|
||||
"active_servers": uint64(1),
|
||||
"backup_servers": uint64(0),
|
||||
"bin": uint64(5228218),
|
||||
"bout": uint64(303747244),
|
||||
"check_code": uint64(200),
|
||||
"check_duration": uint64(3),
|
||||
"check_fall": uint64(3),
|
||||
"check_health": uint64(4),
|
||||
"check_rise": uint64(2),
|
||||
"check_status": "L7OK",
|
||||
"chkdown": uint64(84),
|
||||
"chkfail": uint64(559),
|
||||
"cli_abort": uint64(690),
|
||||
"ctime": uint64(1),
|
||||
"downtime": uint64(3352),
|
||||
"dresp": uint64(0),
|
||||
"econ": uint64(0),
|
||||
"eresp": uint64(21),
|
||||
"http_response.1xx": uint64(0),
|
||||
"http_response.2xx": uint64(5668),
|
||||
"http_response.3xx": uint64(8710),
|
||||
"http_response.4xx": uint64(140),
|
||||
"http_response.5xx": uint64(0),
|
||||
"http_response.other": uint64(0),
|
||||
"iid": uint64(4),
|
||||
"last_chk": "OK",
|
||||
"lastchg": uint64(1036557),
|
||||
"lastsess": int64(1342),
|
||||
"lbtot": uint64(9481),
|
||||
"mode": "http",
|
||||
"pid": uint64(1),
|
||||
"qcur": uint64(0),
|
||||
"qmax": uint64(0),
|
||||
"qtime": uint64(1268),
|
||||
"rate": uint64(0),
|
||||
"rate_max": uint64(2),
|
||||
"rtime": uint64(2908),
|
||||
"sid": uint64(1),
|
||||
"scur": uint64(0),
|
||||
"slim": uint64(2),
|
||||
"smax": uint64(2),
|
||||
"srv_abort": uint64(0),
|
||||
"status": "UP",
|
||||
"stot": uint64(14539),
|
||||
"ttime": uint64(4500),
|
||||
"weight": uint64(1),
|
||||
"wredis": uint64(0),
|
||||
"wretr": uint64(0),
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// Can obtain from official haproxy demo: 'http://demo.haproxy.org/;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,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,32,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,
|
||||
# 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,agent_status,agent_code,agent_duration,check_desc,agent_desc,check_rise,check_fall,check_health,agent_rise,agent_fall,agent_health,addr,cookie,mode,algo,conn_rate,conn_rate_max,conn_tot,intercepted,dcon,dses,
|
||||
http-in,FRONTEND,,,3,100,100,2639994,813557487,65937668635,505252,0,47567,,,,,OPEN,,,,,,,,,1,2,0,,,,0,1,0,157,,,,0,1514640,606647,136264,496535,14948,,1,155,2754255,,,36370569635,17435137766,0,642264,,,,,,,,,,,,,,,,,,,,,http,,1,157,2649922,339471,0,0,
|
||||
http-in,IPv4-direct,,,3,41,100,349801,57445827,1503928881,269899,0,287,,,,,OPEN,,,,,,,,,1,2,1,,,,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,http,,,,,,0,0,
|
||||
http-in,IPv4-cached,,,0,33,100,1786155,644395819,57905460294,60511,0,1,,,,,OPEN,,,,,,,,,1,2,2,,,,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,http,,,,,,0,0,
|
||||
http-in,IPv6-direct,,,0,100,100,325619,92414745,6205208728,3399,0,47279,,,,,OPEN,,,,,,,,,1,2,3,,,,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,http,,,,,,0,0,
|
||||
http-in,local,,,0,0,100,0,0,0,0,0,0,,,,,OPEN,,,,,,,,,1,2,4,,,,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,http,,,,,,0,0,
|
||||
http-in,local-https,,,0,5,100,188347,19301096,323070732,171443,0,0,,,,,OPEN,,,,,,,,,1,2,5,,,,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,http,,,,,,0,0,
|
||||
www,www,0,0,0,20,20,1719698,672044109,64806076656,,0,,0,5285,22,0,UP,1,1,0,561,84,1036557,3356,,1,3,1,,1715117,,2,0,,45,L7OK,200,5,671,1144889,481714,87038,4,0,,,,,105016,167,,,,,5,OK,,0,5,16,1167,,,,Layer7 check passed,,2,3,4,,,,,,http,,,,,,,,
|
||||
www,bck,0,0,0,10,10,1483,537137,7544118,,0,,0,0,0,0,UP,1,0,1,4,0,5218087,0,,1,3,2,,1371,,2,0,,17,L7OK,200,2,0,629,99,755,0,0,,,,,16,0,,,,,1036557,OK,,756,1,13,1184,,,,Layer7 check passed,,2,5,6,,,,,,http,,,,,,,,
|
||||
www,BACKEND,0,25,0,46,100,1721835,674684790,64813732170,314,0,,130,5285,22,0,UP,1,1,1,,0,5218087,0,,1,3,0,,1716488,,1,0,,45,,,,0,1145518,481813,88664,5719,121,,,,1721835,105172,167,35669268059,17250148556,0,556042,5,,,0,5,16,1167,,,,,,,,,,,,,,http,,,,,,,,
|
||||
git,www,0,0,0,2,2,14539,5228218,303747244,,0,,0,21,0,0,UP,1,1,0,559,84,1036557,3352,,1,4,1,,9481,,2,0,,2,L7OK,200,3,0,5668,8710,140,0,0,,,,,690,0,,,,,1342,OK,,1268,1,2908,4500,,,,Layer7 check passed,,2,3,4,,,,,,http,,,,,,,,
|
||||
git,bck,0,0,0,0,2,0,0,0,,0,,0,0,0,0,UP,1,0,1,2,0,5218087,0,,1,4,2,,0,,2,0,,0,L7OK,200,2,0,0,0,0,0,0,,,,,0,0,,,,,-1,OK,,0,0,0,0,,,,Layer7 check passed,,2,3,4,,,,,,http,,,,,,,,
|
||||
git,BACKEND,0,6,0,8,2,14541,8082393,303747668,0,0,,2,21,0,0,UP,1,1,1,,0,5218087,0,,1,4,0,,9481,,1,0,,7,,,,0,5668,8710,140,23,0,,,,14541,690,0,133458298,38104818,0,4379,1342,,,1268,1,2908,4500,,,,,,,,,,,,,,http,,,,,,,,
|
||||
demo,BACKEND,0,0,1,5,20,24063,7876647,659864417,48,0,,1,0,0,0,UP,0,0,0,,0,5218087,,,1,17,0,,0,,1,1,,26,,,,0,23983,21,0,1,57,,,,24062,111,0,567843278,146884392,0,1083,0,,,2706,0,0,887,,,,,,,,,,,,,,http,,,,,,,,
|
||||
`
|
||||
|
||||
@@ -23,6 +23,11 @@ This input plugin will test HTTP/HTTPS connections.
|
||||
# {'fake':'data'}
|
||||
# '''
|
||||
|
||||
## Optional substring or regex match in body of the response
|
||||
## response_string_match = "\"service_status\": \"up\""
|
||||
## response_string_match = "ok"
|
||||
## response_string_match = "\".*_status\".?:.?\"up\""
|
||||
|
||||
## Optional SSL Config
|
||||
# ssl_ca = "/etc/telegraf/ca.pem"
|
||||
# ssl_cert = "/etc/telegraf/cert.pem"
|
||||
|
||||
@@ -3,8 +3,11 @@ package http_response
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -15,12 +18,14 @@ import (
|
||||
|
||||
// HTTPResponse struct
|
||||
type HTTPResponse struct {
|
||||
Address string
|
||||
Body string
|
||||
Method string
|
||||
ResponseTimeout internal.Duration
|
||||
Headers map[string]string
|
||||
FollowRedirects bool
|
||||
Address string
|
||||
Body string
|
||||
Method string
|
||||
ResponseTimeout internal.Duration
|
||||
Headers map[string]string
|
||||
FollowRedirects bool
|
||||
ResponseStringMatch string
|
||||
compiledStringMatch *regexp.Regexp
|
||||
|
||||
// Path to CA file
|
||||
SSLCA string `toml:"ssl_ca"`
|
||||
@@ -54,6 +59,11 @@ var sampleConfig = `
|
||||
# {'fake':'data'}
|
||||
# '''
|
||||
|
||||
## Optional substring or regex match in body of the response
|
||||
## response_string_match = "\"service_status\": \"up\""
|
||||
## response_string_match = "ok"
|
||||
## response_string_match = "\".*_status\".?:.?\"up\""
|
||||
|
||||
## Optional SSL Config
|
||||
# ssl_ca = "/etc/telegraf/ca.pem"
|
||||
# ssl_cert = "/etc/telegraf/cert.pem"
|
||||
@@ -137,6 +147,35 @@ func (h *HTTPResponse) HTTPGather() (map[string]interface{}, error) {
|
||||
}
|
||||
fields["response_time"] = time.Since(start).Seconds()
|
||||
fields["http_response_code"] = resp.StatusCode
|
||||
|
||||
// Check the response for a regex match.
|
||||
if h.ResponseStringMatch != "" {
|
||||
|
||||
// Compile once and reuse
|
||||
if h.compiledStringMatch == nil {
|
||||
h.compiledStringMatch = regexp.MustCompile(h.ResponseStringMatch)
|
||||
if err != nil {
|
||||
log.Printf("E! Failed to compile regular expression %s : %s", h.ResponseStringMatch, err)
|
||||
fields["response_string_match"] = 0
|
||||
return fields, nil
|
||||
}
|
||||
}
|
||||
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("E! Failed to read body of HTTP Response : %s", err)
|
||||
fields["response_string_match"] = 0
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
if h.compiledStringMatch.Match(bodyBytes) {
|
||||
fields["response_string_match"] = 1
|
||||
} else {
|
||||
fields["response_string_match"] = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ func setUpTestMux() http.Handler {
|
||||
mux.HandleFunc("/good", func(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, "hit the good page!")
|
||||
})
|
||||
mux.HandleFunc("/jsonresponse", func(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, "\"service_status\": \"up\", \"healthy\" : \"true\"")
|
||||
})
|
||||
mux.HandleFunc("/badredirect", func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, "/badredirect", http.StatusMovedPermanently)
|
||||
})
|
||||
@@ -236,6 +239,87 @@ func TestBody(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringMatch(t *testing.T) {
|
||||
mux := setUpTestMux()
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
h := &HTTPResponse{
|
||||
Address: ts.URL + "/good",
|
||||
Body: "{ 'test': 'data'}",
|
||||
Method: "GET",
|
||||
ResponseStringMatch: "hit the good page",
|
||||
ResponseTimeout: internal.Duration{Duration: time.Second * 20},
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
FollowRedirects: true,
|
||||
}
|
||||
fields, err := h.HTTPGather()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, fields)
|
||||
if assert.NotNil(t, fields["http_response_code"]) {
|
||||
assert.Equal(t, http.StatusOK, fields["http_response_code"])
|
||||
}
|
||||
assert.Equal(t, 1, fields["response_string_match"])
|
||||
assert.NotNil(t, fields["response_time"])
|
||||
|
||||
}
|
||||
|
||||
func TestStringMatchJson(t *testing.T) {
|
||||
mux := setUpTestMux()
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
h := &HTTPResponse{
|
||||
Address: ts.URL + "/jsonresponse",
|
||||
Body: "{ 'test': 'data'}",
|
||||
Method: "GET",
|
||||
ResponseStringMatch: "\"service_status\": \"up\"",
|
||||
ResponseTimeout: internal.Duration{Duration: time.Second * 20},
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
FollowRedirects: true,
|
||||
}
|
||||
fields, err := h.HTTPGather()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, fields)
|
||||
if assert.NotNil(t, fields["http_response_code"]) {
|
||||
assert.Equal(t, http.StatusOK, fields["http_response_code"])
|
||||
}
|
||||
assert.Equal(t, 1, fields["response_string_match"])
|
||||
assert.NotNil(t, fields["response_time"])
|
||||
|
||||
}
|
||||
|
||||
func TestStringMatchFail(t *testing.T) {
|
||||
mux := setUpTestMux()
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
h := &HTTPResponse{
|
||||
Address: ts.URL + "/good",
|
||||
Body: "{ 'test': 'data'}",
|
||||
Method: "GET",
|
||||
ResponseStringMatch: "hit the bad page",
|
||||
ResponseTimeout: internal.Duration{Duration: time.Second * 20},
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
FollowRedirects: true,
|
||||
}
|
||||
fields, err := h.HTTPGather()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, fields)
|
||||
if assert.NotNil(t, fields["http_response_code"]) {
|
||||
assert.Equal(t, http.StatusOK, fields["http_response_code"])
|
||||
}
|
||||
assert.Equal(t, 0, fields["response_string_match"])
|
||||
assert.NotNil(t, fields["response_time"])
|
||||
|
||||
}
|
||||
|
||||
func TestTimeout(t *testing.T) {
|
||||
mux := setUpTestMux()
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
@@ -4,33 +4,50 @@ Get bare metal metrics using the command line utility `ipmitool`
|
||||
|
||||
see ipmitool(https://sourceforge.net/projects/ipmitool/files/ipmitool/)
|
||||
|
||||
The plugin will use the following command to collect remote host sensor stats:
|
||||
If no servers are specified, the plugin will query the local machine sensor stats via the following command:
|
||||
|
||||
ipmitool -I lan -H 192.168.1.1 -U USERID -P PASSW0RD sdr
|
||||
```
|
||||
ipmitool sdr
|
||||
```
|
||||
|
||||
When one or more servers are specified, the plugin will use the following command to collect remote host sensor stats:
|
||||
|
||||
```
|
||||
ipmitool -I lan -H SERVER -U USERID -P PASSW0RD sdr
|
||||
```
|
||||
|
||||
## Measurements
|
||||
|
||||
- ipmi_sensor:
|
||||
|
||||
* Tags: `name`, `server`, `unit`
|
||||
* Tags: `name`, `unit`
|
||||
* Fields:
|
||||
- status
|
||||
- value
|
||||
|
||||
The `server` tag will be made available when retrieving stats from remote server(s).
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml
|
||||
# Read metrics from the bare metal servers via IPMI
|
||||
[[inputs.ipmi_sensor]]
|
||||
## specify servers via a url matching:
|
||||
## optionally specify the path to the ipmitool executable
|
||||
# path = "/usr/bin/ipmitool"
|
||||
#
|
||||
## optionally specify one or more servers via a url matching
|
||||
## [username[:password]@][protocol[(address)]]
|
||||
## e.g.
|
||||
## root:passwd@lan(127.0.0.1)
|
||||
##
|
||||
servers = ["USERID:PASSW0RD@lan(10.20.2.203)"]
|
||||
## if no servers are specified, local machine sensor stats will be queried
|
||||
##
|
||||
# servers = ["USERID:PASSW0RD@lan(192.168.1.1)"]
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
When retrieving stats from a remote server:
|
||||
```
|
||||
> ipmi_sensor,server=10.20.2.203,unit=degrees_c,name=ambient_temp status=1i,value=20 1458488465012559455
|
||||
> ipmi_sensor,server=10.20.2.203,unit=feet,name=altitude status=1i,value=80 1458488465012688613
|
||||
@@ -40,3 +57,14 @@ ipmitool -I lan -H 192.168.1.1 -U USERID -P PASSW0RD sdr
|
||||
> ipmi_sensor,server=10.20.2.203,unit=rpm,name=fan_1a_tach status=1i,value=2610 1458488465013137932
|
||||
> ipmi_sensor,server=10.20.2.203,unit=rpm,name=fan_1b_tach status=1i,value=1775 1458488465013279896
|
||||
```
|
||||
|
||||
When retrieving stats from the local machine (no server specified):
|
||||
```
|
||||
> ipmi_sensor,unit=degrees_c,name=ambient_temp status=1i,value=20 1458488465012559455
|
||||
> ipmi_sensor,unit=feet,name=altitude status=1i,value=80 1458488465012688613
|
||||
> ipmi_sensor,unit=watts,name=avg_power status=1i,value=220 1458488465012776511
|
||||
> ipmi_sensor,unit=volts,name=planar_3.3v status=1i,value=3.28 1458488465012861875
|
||||
> ipmi_sensor,unit=volts,name=planar_vbat status=1i,value=3.04 1458488465013072508
|
||||
> ipmi_sensor,unit=rpm,name=fan_1a_tach status=1i,value=2610 1458488465013137932
|
||||
> ipmi_sensor,unit=rpm,name=fan_1b_tach status=1i,value=1775 1458488465013279896
|
||||
```
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package ipmi_sensor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
)
|
||||
|
||||
type CommandRunner struct{}
|
||||
|
||||
func (t CommandRunner) cmd(conn *Connection, args ...string) *exec.Cmd {
|
||||
path := conn.Path
|
||||
opts := append(conn.options(), args...)
|
||||
|
||||
if path == "" {
|
||||
path = "ipmitool"
|
||||
}
|
||||
|
||||
return exec.Command(path, opts...)
|
||||
}
|
||||
|
||||
func (t CommandRunner) Run(conn *Connection, args ...string) (string, error) {
|
||||
cmd := t.cmd(conn, args...)
|
||||
|
||||
output, err := internal.CombinedOutputTimeout(cmd, time.Second*5)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("run %s %s: %s (%s)",
|
||||
cmd.Path, strings.Join(cmd.Args, " "), string(output), err)
|
||||
}
|
||||
|
||||
return string(output), err
|
||||
}
|
||||
@@ -12,7 +12,6 @@ type Connection struct {
|
||||
Hostname string
|
||||
Username string
|
||||
Password string
|
||||
Path string
|
||||
Port int
|
||||
Interface string
|
||||
}
|
||||
|
||||
@@ -1,48 +1,62 @@
|
||||
package ipmi_sensor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
var (
|
||||
execCommand = exec.Command // execCommand is used to mock commands in tests.
|
||||
)
|
||||
|
||||
type Ipmi struct {
|
||||
path string
|
||||
Servers []string
|
||||
runner Runner
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
## specify servers via a url matching:
|
||||
## optionally specify the path to the ipmitool executable
|
||||
# path = "/usr/bin/ipmitool"
|
||||
#
|
||||
## optionally specify one or more servers via a url matching
|
||||
## [username[:password]@][protocol[(address)]]
|
||||
## e.g.
|
||||
## root:passwd@lan(127.0.0.1)
|
||||
##
|
||||
servers = ["USERID:PASSW0RD@lan(192.168.1.1)"]
|
||||
## if no servers are specified, local machine sensor stats will be queried
|
||||
##
|
||||
# servers = ["USERID:PASSW0RD@lan(192.168.1.1)"]
|
||||
`
|
||||
|
||||
func NewIpmi() *Ipmi {
|
||||
return &Ipmi{
|
||||
runner: CommandRunner{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Ipmi) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (m *Ipmi) Description() string {
|
||||
return "Read metrics from one or many bare metal servers"
|
||||
return "Read metrics from the bare metal servers via IPMI"
|
||||
}
|
||||
|
||||
func (m *Ipmi) Gather(acc telegraf.Accumulator) error {
|
||||
if m.runner == nil {
|
||||
m.runner = CommandRunner{}
|
||||
if len(m.path) == 0 {
|
||||
return fmt.Errorf("ipmitool not found: verify that ipmitool is installed and that ipmitool is in your PATH")
|
||||
}
|
||||
for _, serv := range m.Servers {
|
||||
err := m.gatherServer(serv, acc)
|
||||
|
||||
if len(m.Servers) > 0 {
|
||||
for _, server := range m.Servers {
|
||||
err := m.parse(acc, server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err := m.parse(acc, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -51,17 +65,26 @@ func (m *Ipmi) Gather(acc telegraf.Accumulator) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Ipmi) gatherServer(serv string, acc telegraf.Accumulator) error {
|
||||
conn := NewConnection(serv)
|
||||
func (m *Ipmi) parse(acc telegraf.Accumulator, server string) error {
|
||||
opts := make([]string, 0)
|
||||
hostname := ""
|
||||
|
||||
res, err := m.runner.Run(conn, "sdr")
|
||||
if server != "" {
|
||||
conn := NewConnection(server)
|
||||
hostname = conn.Hostname
|
||||
opts = conn.options()
|
||||
}
|
||||
|
||||
opts = append(opts, "sdr")
|
||||
cmd := execCommand(m.path, opts...)
|
||||
out, err := internal.CombinedOutputTimeout(cmd, time.Second*5)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to run command %s: %s - %s", strings.Join(cmd.Args, " "), err, string(out))
|
||||
}
|
||||
|
||||
// each line will look something like
|
||||
// Planar VBAT | 3.05 Volts | ok
|
||||
lines := strings.Split(res, "\n")
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
vals := strings.Split(lines[i], "|")
|
||||
if len(vals) != 3 {
|
||||
@@ -69,8 +92,12 @@ func (m *Ipmi) gatherServer(serv string, acc telegraf.Accumulator) error {
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"server": conn.Hostname,
|
||||
"name": transform(vals[0]),
|
||||
"name": transform(vals[0]),
|
||||
}
|
||||
|
||||
// tag the server is we have one
|
||||
if hostname != "" {
|
||||
tags["server"] = hostname
|
||||
}
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
@@ -99,10 +126,6 @@ func (m *Ipmi) gatherServer(serv string, acc telegraf.Accumulator) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type Runner interface {
|
||||
Run(conn *Connection, args ...string) (string, error)
|
||||
}
|
||||
|
||||
func Atofloat(val string) float64 {
|
||||
f, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
@@ -123,7 +146,12 @@ func transform(s string) string {
|
||||
}
|
||||
|
||||
func init() {
|
||||
m := Ipmi{}
|
||||
path, _ := exec.LookPath("ipmitool")
|
||||
if len(path) > 0 {
|
||||
m.path = path
|
||||
}
|
||||
inputs.Add("ipmi_sensor", func() telegraf.Input {
|
||||
return &Ipmi{}
|
||||
return &m
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ipmi_sensor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
@@ -8,10 +11,219 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const serv = "USERID:PASSW0RD@lan(192.168.1.1)"
|
||||
func TestGather(t *testing.T) {
|
||||
i := &Ipmi{
|
||||
Servers: []string{"USERID:PASSW0RD@lan(192.168.1.1)"},
|
||||
path: "ipmitool",
|
||||
}
|
||||
// overwriting exec commands with mock commands
|
||||
execCommand = fakeExecCommand
|
||||
var acc testutil.Accumulator
|
||||
|
||||
const cmdReturn = `
|
||||
Ambient Temp | 20 degrees C | ok
|
||||
err := i.Gather(&acc)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, acc.NFields(), 266, "non-numeric measurements should be ignored")
|
||||
|
||||
conn := NewConnection(i.Servers[0])
|
||||
assert.Equal(t, "USERID", conn.Username)
|
||||
assert.Equal(t, "lan", conn.Interface)
|
||||
|
||||
var testsWithServer = []struct {
|
||||
fields map[string]interface{}
|
||||
tags map[string]string
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(20),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "ambient_temp",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "degrees_c",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(80),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "altitude",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "feet",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(210),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "avg_power",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "watts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(4.9),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_5v",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(3.05),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_vbat",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(2610),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1a_tach",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(1775),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1b_tach",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testsWithServer {
|
||||
acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
|
||||
}
|
||||
|
||||
i = &Ipmi{
|
||||
path: "ipmitool",
|
||||
}
|
||||
|
||||
err = i.Gather(&acc)
|
||||
|
||||
var testsWithoutServer = []struct {
|
||||
fields map[string]interface{}
|
||||
tags map[string]string
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(20),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "ambient_temp",
|
||||
"unit": "degrees_c",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(80),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "altitude",
|
||||
"unit": "feet",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(210),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "avg_power",
|
||||
"unit": "watts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(4.9),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_5v",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(3.05),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_vbat",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(2610),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1a_tach",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(1775),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1b_tach",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testsWithoutServer {
|
||||
acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
|
||||
}
|
||||
}
|
||||
|
||||
// fackeExecCommand is a helper function that mock
|
||||
// the exec.Command call (and call the test binary)
|
||||
func fakeExecCommand(command string, args ...string) *exec.Cmd {
|
||||
cs := []string{"-test.run=TestHelperProcess", "--", command}
|
||||
cs = append(cs, args...)
|
||||
cmd := exec.Command(os.Args[0], cs...)
|
||||
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TestHelperProcess isn't a real test. It's used to mock exec.Command
|
||||
// For example, if you run:
|
||||
// GO_WANT_HELPER_PROCESS=1 go test -test.run=TestHelperProcess -- chrony tracking
|
||||
// it returns below mockData.
|
||||
func TestHelperProcess(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
mockData := `Ambient Temp | 20 degrees C | ok
|
||||
Altitude | 80 feet | ok
|
||||
Avg Power | 210 Watts | ok
|
||||
Planar 3.3V | 3.29 Volts | ok
|
||||
@@ -146,130 +358,18 @@ PCI 5 | 0x00 | ok
|
||||
OS RealTime Mod | 0x00 | ok
|
||||
`
|
||||
|
||||
type runnerMock struct {
|
||||
out string
|
||||
err error
|
||||
}
|
||||
args := os.Args
|
||||
|
||||
// Previous arguments are tests stuff, that looks like :
|
||||
// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
|
||||
cmd, args := args[3], args[4:]
|
||||
|
||||
if cmd == "ipmitool" {
|
||||
fmt.Fprint(os.Stdout, mockData)
|
||||
} else {
|
||||
fmt.Fprint(os.Stdout, "command not found")
|
||||
os.Exit(1)
|
||||
|
||||
func newRunnerMock(out string, err error) Runner {
|
||||
return &runnerMock{
|
||||
out: out,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (r runnerMock) Run(conn *Connection, args ...string) (out string, err error) {
|
||||
if r.err != nil {
|
||||
return out, r.err
|
||||
}
|
||||
return r.out, nil
|
||||
}
|
||||
|
||||
func TestIpmi(t *testing.T) {
|
||||
i := &Ipmi{
|
||||
Servers: []string{"USERID:PASSW0RD@lan(192.168.1.1)"},
|
||||
runner: newRunnerMock(cmdReturn, nil),
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
|
||||
err := i.Gather(&acc)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, acc.NFields(), 266, "non-numeric measurements should be ignored")
|
||||
|
||||
var tests = []struct {
|
||||
fields map[string]interface{}
|
||||
tags map[string]string
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(20),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "ambient_temp",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "degrees_c",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(80),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "altitude",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "feet",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(210),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "avg_power",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "watts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(4.9),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_5v",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(3.05),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_vbat",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(2610),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1a_tach",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(1775),
|
||||
"status": int(1),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1b_tach",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIpmiConnection(t *testing.T) {
|
||||
conn := NewConnection(serv)
|
||||
assert.Equal(t, "USERID", conn.Username)
|
||||
assert.Equal(t, "lan", conn.Interface)
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -30,11 +30,17 @@ You may edit your sudo configuration with the following:
|
||||
telegraf ALL=(root) NOPASSWD: /usr/bin/iptables -nvL *
|
||||
```
|
||||
|
||||
### Using IPtables lock feature
|
||||
|
||||
Defining multiple instances of this plugin in telegraf.conf can lead to concurrent IPtables access resulting in "ERROR in input [inputs.iptables]: exit status 4" messages in telegraf.log and missing metrics. Setting 'use_lock = true' in the plugin configuration will run IPtables with the '-w' switch, allowing a lock usage to prevent this error.
|
||||
|
||||
### Configuration:
|
||||
|
||||
```toml
|
||||
# use sudo to run iptables
|
||||
use_sudo = false
|
||||
# run iptables with the lock option
|
||||
use_lock = false
|
||||
# defines the table to monitor:
|
||||
table = "filter"
|
||||
# defines the chains to monitor:
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
// Iptables is a telegraf plugin to gather packets and bytes throughput from Linux's iptables packet filter.
|
||||
type Iptables struct {
|
||||
UseSudo bool
|
||||
UseLock bool
|
||||
Table string
|
||||
Chains []string
|
||||
lister chainLister
|
||||
@@ -32,8 +33,11 @@ func (ipt *Iptables) SampleConfig() string {
|
||||
## iptables require root access on most systems.
|
||||
## Setting 'use_sudo' to true will make use of sudo to run iptables.
|
||||
## Users must configure sudo to allow telegraf user to run iptables with no password.
|
||||
## iptables can be restricted to only list command "iptables -nvL"
|
||||
## iptables can be restricted to only list command "iptables -nvL"
|
||||
use_sudo = false
|
||||
## Setting 'use_lock' to true runs iptables with the "-w" option.
|
||||
## Adjust your sudo settings appropriately if using this option ("iptables -wnvl")
|
||||
use_lock = false
|
||||
## defines the table to monitor:
|
||||
table = "filter"
|
||||
## defines the chains to monitor:
|
||||
@@ -75,7 +79,11 @@ func (ipt *Iptables) chainList(table, chain string) (string, error) {
|
||||
name = "sudo"
|
||||
args = append(args, iptablePath)
|
||||
}
|
||||
args = append(args, "-nvL", chain, "-t", table, "-x")
|
||||
iptablesBaseArgs := "-nvL"
|
||||
if ipt.UseLock {
|
||||
iptablesBaseArgs = "-wnvL"
|
||||
}
|
||||
args = append(args, iptablesBaseArgs, chain, "-t", table, "-x")
|
||||
c := exec.Command(name, args...)
|
||||
out, err := c.Output()
|
||||
return string(out), err
|
||||
|
||||
@@ -45,7 +45,7 @@ type CPUMetrics struct {
|
||||
// PodMetrics contains metric data on a given pod
|
||||
type PodMetrics struct {
|
||||
PodRef PodReference `json:"podRef"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
StartTime *time.Time `json:"startTime"`
|
||||
Containers []ContainerMetrics `json:"containers"`
|
||||
Network NetworkMetrics `json:"network"`
|
||||
Volumes []VolumeMetrics `json:"volume"`
|
||||
|
||||
@@ -92,6 +92,29 @@ func TestKubernetesStats(t *testing.T) {
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "kubernetes_pod_container", fields, tags)
|
||||
|
||||
fields = map[string]interface{}{
|
||||
"cpu_usage_nanocores": int64(846503),
|
||||
"cpu_usage_core_nanoseconds": int64(56507553554),
|
||||
"memory_usage_bytes": int64(0),
|
||||
"memory_working_set_bytes": int64(0),
|
||||
"memory_rss_bytes": int64(0),
|
||||
"memory_page_faults": int64(0),
|
||||
"memory_major_page_faults": int64(0),
|
||||
"rootfs_available_bytes": int64(0),
|
||||
"rootfs_capacity_bytes": int64(0),
|
||||
"rootfs_used_bytes": int64(0),
|
||||
"logsfs_avaialble_bytes": int64(0),
|
||||
"logsfs_capacity_bytes": int64(0),
|
||||
"logsfs_used_bytes": int64(0),
|
||||
}
|
||||
tags = map[string]string{
|
||||
"node_name": "node1",
|
||||
"container_name": "stopped-container",
|
||||
"namespace": "foons",
|
||||
"pod_name": "stopped-pod",
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "kubernetes_pod_container", fields, tags)
|
||||
|
||||
fields = map[string]interface{}{
|
||||
"available_bytes": int64(7903948800),
|
||||
"capacity_bytes": int64(7903961088),
|
||||
@@ -284,6 +307,25 @@ var response = `
|
||||
"name": "volume4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"podRef": {
|
||||
"name": "stopped-pod",
|
||||
"namespace": "foons",
|
||||
"uid": "da7c1865-d67d-4688-b679-c485ed44b2aa"
|
||||
},
|
||||
"startTime": null,
|
||||
"containers": [
|
||||
{
|
||||
"name": "stopped-container",
|
||||
"startTime": "2016-09-26T18:46:43Z",
|
||||
"cpu": {
|
||||
"time": "2016-09-27T16:57:32Z",
|
||||
"usageNanoCores": 846503,
|
||||
"usageCoreNanoSeconds": 56507553554
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
@@ -26,7 +26,7 @@ type LogParserPlugin struct {
|
||||
Files []string
|
||||
FromBeginning bool
|
||||
|
||||
tailers []*tail.Tail
|
||||
tailers map[string]*tail.Tail
|
||||
lines chan string
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
@@ -46,7 +46,9 @@ const sampleConfig = `
|
||||
## /var/log/*/*.log -> find all .log files with a parent dir in /var/log
|
||||
## /var/log/apache.log -> only tail the apache log file
|
||||
files = ["/var/log/apache/access.log"]
|
||||
## Read file from beginning.
|
||||
## Read files that currently exist from the beginning. Files that are created
|
||||
## while telegraf is running (and that match the "files" globs) will always
|
||||
## be read from the beginning.
|
||||
from_beginning = false
|
||||
|
||||
## Parse logstash-style "grok" patterns:
|
||||
@@ -77,7 +79,11 @@ func (l *LogParserPlugin) Description() string {
|
||||
}
|
||||
|
||||
func (l *LogParserPlugin) Gather(acc telegraf.Accumulator) error {
|
||||
return nil
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
// always start from the beginning of files that appear while we're running
|
||||
return l.tailNewfiles(true)
|
||||
}
|
||||
|
||||
func (l *LogParserPlugin) Start(acc telegraf.Accumulator) error {
|
||||
@@ -87,6 +93,7 @@ func (l *LogParserPlugin) Start(acc telegraf.Accumulator) error {
|
||||
l.acc = acc
|
||||
l.lines = make(chan string, 1000)
|
||||
l.done = make(chan struct{})
|
||||
l.tailers = make(map[string]*tail.Tail)
|
||||
|
||||
// Looks for fields which implement LogParser interface
|
||||
l.parsers = []LogParser{}
|
||||
@@ -121,14 +128,22 @@ func (l *LogParserPlugin) Start(acc telegraf.Accumulator) error {
|
||||
return err
|
||||
}
|
||||
|
||||
l.wg.Add(1)
|
||||
go l.parser()
|
||||
|
||||
return l.tailNewfiles(l.FromBeginning)
|
||||
}
|
||||
|
||||
// check the globs against files on disk, and start tailing any new files.
|
||||
// Assumes l's lock is held!
|
||||
func (l *LogParserPlugin) tailNewfiles(fromBeginning bool) error {
|
||||
var seek tail.SeekInfo
|
||||
if !l.FromBeginning {
|
||||
if !fromBeginning {
|
||||
seek.Whence = 2
|
||||
seek.Offset = 0
|
||||
}
|
||||
|
||||
l.wg.Add(1)
|
||||
go l.parser()
|
||||
errChan := errchan.New(len(l.Files))
|
||||
|
||||
// Create a "tailer" for each file
|
||||
for _, filepath := range l.Files {
|
||||
@@ -139,7 +154,13 @@ func (l *LogParserPlugin) Start(acc telegraf.Accumulator) error {
|
||||
}
|
||||
files := g.Match()
|
||||
errChan = errchan.New(len(files))
|
||||
|
||||
for file, _ := range files {
|
||||
if _, ok := l.tailers[file]; ok {
|
||||
// we're already tailing this file
|
||||
continue
|
||||
}
|
||||
|
||||
tailer, err := tail.TailFile(file,
|
||||
tail.Config{
|
||||
ReOpen: true,
|
||||
@@ -152,7 +173,7 @@ func (l *LogParserPlugin) Start(acc telegraf.Accumulator) error {
|
||||
// create a goroutine for each "tailer"
|
||||
l.wg.Add(1)
|
||||
go l.receiver(tailer)
|
||||
l.tailers = append(l.tailers, tailer)
|
||||
l.tailers[file] = tailer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +187,7 @@ func (l *LogParserPlugin) receiver(tailer *tail.Tail) {
|
||||
|
||||
var line *tail.Line
|
||||
for line = range tailer.Lines {
|
||||
|
||||
if line.Err != nil {
|
||||
log.Printf("E! Error tailing file %s, Error: %s\n",
|
||||
tailer.Filename, line.Err)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package logparser
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -80,6 +82,47 @@ func TestGrokParseLogFiles(t *testing.T) {
|
||||
map[string]string{})
|
||||
}
|
||||
|
||||
func TestGrokParseLogFilesAppearLater(t *testing.T) {
|
||||
emptydir, err := ioutil.TempDir("", "TestGrokParseLogFilesAppearLater")
|
||||
defer os.RemoveAll(emptydir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
thisdir := getCurrentDir()
|
||||
p := &grok.Parser{
|
||||
Patterns: []string{"%{TEST_LOG_A}", "%{TEST_LOG_B}"},
|
||||
CustomPatternFiles: []string{thisdir + "grok/testdata/test-patterns"},
|
||||
}
|
||||
|
||||
logparser := &LogParserPlugin{
|
||||
FromBeginning: true,
|
||||
Files: []string{emptydir + "/*.log"},
|
||||
GrokParser: p,
|
||||
}
|
||||
|
||||
acc := testutil.Accumulator{}
|
||||
assert.NoError(t, logparser.Start(&acc))
|
||||
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
assert.Equal(t, acc.NFields(), 0)
|
||||
|
||||
os.Symlink(
|
||||
thisdir+"grok/testdata/test_a.log",
|
||||
emptydir+"/test_a.log")
|
||||
assert.NoError(t, logparser.Gather(&acc))
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
|
||||
logparser.Stop()
|
||||
|
||||
acc.AssertContainsTaggedFields(t, "logparser_grok",
|
||||
map[string]interface{}{
|
||||
"clientip": "192.168.1.1",
|
||||
"myfloat": float64(1.25),
|
||||
"response_time": int64(5432),
|
||||
"myint": int64(101),
|
||||
},
|
||||
map[string]string{"response_code": "200"})
|
||||
}
|
||||
|
||||
// Test that test_a.log line gets parsed even though we don't have the correct
|
||||
// pattern available for test_b.log
|
||||
func TestGrokParseLogFilesOneBad(t *testing.T) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
const (
|
||||
testMsg = "cpu_load_short,host=server01 value=23422.0 1422568543702900257\n"
|
||||
testMsgNeg = "cpu_load_short,host=server01 value=-23422.0 1422568543702900257\n"
|
||||
testMsgGraphite = "cpu.load.short.graphite 23422 1454780029"
|
||||
testMsgJSON = "{\"a\": 5, \"b\": {\"c\": 6}}\n"
|
||||
invalidMsg = "cpu_load_short,host=server01 1422568543702900257\n"
|
||||
@@ -76,13 +77,28 @@ func TestPersistentClientIDFail(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// Test that the parser parses NATS messages into metrics
|
||||
func TestRunParser(t *testing.T) {
|
||||
n, in := newTestMQTTConsumer()
|
||||
acc := testutil.Accumulator{}
|
||||
n.acc = &acc
|
||||
defer close(n.done)
|
||||
|
||||
n.parser, _ = parsers.NewInfluxParser()
|
||||
go n.receiver()
|
||||
in <- mqttMsg(testMsgNeg)
|
||||
time.Sleep(time.Millisecond * 250)
|
||||
|
||||
if a := acc.NFields(); a != 1 {
|
||||
t.Errorf("got %v, expected %v", a, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunParserNegativeNumber(t *testing.T) {
|
||||
n, in := newTestMQTTConsumer()
|
||||
acc := testutil.Accumulator{}
|
||||
n.acc = &acc
|
||||
defer close(n.done)
|
||||
|
||||
n.parser, _ = parsers.NewInfluxParser()
|
||||
go n.receiver()
|
||||
in <- mqttMsg(testMsg)
|
||||
|
||||
@@ -7,7 +7,9 @@ This plugin gathers the statistic data from MySQL server
|
||||
* Slave statuses
|
||||
* Binlog size
|
||||
* Process list
|
||||
* User Statistics
|
||||
* Info schema auto increment columns
|
||||
* InnoDB metrics
|
||||
* Table I/O waits
|
||||
* Index I/O waits
|
||||
* Perf Schema table lock waits
|
||||
@@ -44,9 +46,15 @@ This plugin gathers the statistic data from MySQL server
|
||||
## gather thread state counts from INFORMATION_SCHEMA.PROCESSLIST
|
||||
gather_process_list = true
|
||||
#
|
||||
## gather thread state counts from INFORMATION_SCHEMA.USER_STATISTICS
|
||||
gather_user_statistics = true
|
||||
#
|
||||
## gather auto_increment columns and max values from information schema
|
||||
gather_info_schema_auto_inc = true
|
||||
#
|
||||
## gather metrics from INFORMATION_SCHEMA.INNODB_METRICS
|
||||
gather_innodb_metrics = true
|
||||
#
|
||||
## gather metrics from SHOW SLAVE STATUS command output
|
||||
gather_slave_status = true
|
||||
#
|
||||
@@ -89,6 +97,30 @@ Requires to be turned on in configuration.
|
||||
* binary_files_count(int, number)
|
||||
* Process list - connection metrics from processlist for each user. It has the following tags
|
||||
* connections(int, number)
|
||||
* User Statistics - connection metrics from user statistics for each user. It has the following fields
|
||||
* access_denied
|
||||
* binlog_bytes_written
|
||||
* busy_time
|
||||
* bytes_received
|
||||
* bytes_sent
|
||||
* commit_transactions
|
||||
* concurrent_connections
|
||||
* connected_time
|
||||
* cpu_time
|
||||
* denied_connections
|
||||
* empty_queries
|
||||
* hostlost_connections
|
||||
* other_commands
|
||||
* rollback_transactions
|
||||
* rows_fetched
|
||||
* rows_updated
|
||||
* select_commands
|
||||
* server
|
||||
* table_rows_read
|
||||
* total_connections
|
||||
* total_ssl_connections
|
||||
* update_commands
|
||||
* user
|
||||
* Perf Table IO waits - total count and time of I/O waits event for each table
|
||||
and process. It has following fields:
|
||||
* table_io_waits_total_fetch(float, number)
|
||||
@@ -113,6 +145,7 @@ and process. It has following fields:
|
||||
for them. It has following fields:
|
||||
* auto_increment_column(int, number)
|
||||
* auto_increment_column_max(int, number)
|
||||
* InnoDB metrics - all metrics of information_schema.INNODB_METRICS with a status "enabled"
|
||||
* Perf table lock waits - gathers total number and time for SQL and external
|
||||
lock waits events for each table and operation. It has following fields.
|
||||
The unit of fields varies by the tags.
|
||||
@@ -158,6 +191,8 @@ The unit of fields varies by the tags.
|
||||
* server (the host name from which the metrics are gathered)
|
||||
* Process list measurement has following tags
|
||||
* user (username for whom the metrics are gathered)
|
||||
* User Statistics measurement has following tags
|
||||
* user (username for whom the metrics are gathered)
|
||||
* Perf table IO waits measurement has following tags
|
||||
* schema
|
||||
* name (object name for event or process)
|
||||
|
||||
@@ -23,7 +23,9 @@ type Mysql struct {
|
||||
PerfEventsStatementsTimeLimit int64 `toml:"perf_events_statemetns_time_limit"`
|
||||
TableSchemaDatabases []string `toml:"table_schema_databases"`
|
||||
GatherProcessList bool `toml:"gather_process_list"`
|
||||
GatherUserStatistics bool `toml:"gather_user_statistics"`
|
||||
GatherInfoSchemaAutoInc bool `toml:"gather_info_schema_auto_inc"`
|
||||
GatherInnoDBMetrics bool `toml:"gather_innodb_metrics"`
|
||||
GatherSlaveStatus bool `toml:"gather_slave_status"`
|
||||
GatherBinaryLogs bool `toml:"gather_binary_logs"`
|
||||
GatherTableIOWaits bool `toml:"gather_table_io_waits"`
|
||||
@@ -60,9 +62,15 @@ var sampleConfig = `
|
||||
## gather thread state counts from INFORMATION_SCHEMA.PROCESSLIST
|
||||
gather_process_list = true
|
||||
#
|
||||
## gather thread state counts from INFORMATION_SCHEMA.USER_STATISTICS
|
||||
gather_user_statistics = true
|
||||
#
|
||||
## gather auto_increment columns and max values from information schema
|
||||
gather_info_schema_auto_inc = true
|
||||
#
|
||||
## gather metrics from INFORMATION_SCHEMA.INNODB_METRICS
|
||||
gather_innodb_metrics = true
|
||||
#
|
||||
## gather metrics from SHOW SLAVE STATUS command output
|
||||
gather_slave_status = true
|
||||
#
|
||||
@@ -415,6 +423,10 @@ const (
|
||||
WHERE ID != connection_id()
|
||||
GROUP BY command,state
|
||||
ORDER BY null`
|
||||
infoSchemaUserStatisticsQuery = `
|
||||
SELECT *,count(*)
|
||||
FROM information_schema.user_statistics
|
||||
GROUP BY user`
|
||||
infoSchemaAutoIncQuery = `
|
||||
SELECT table_schema, table_name, column_name, auto_increment,
|
||||
CAST(pow(2, case data_type
|
||||
@@ -427,6 +439,11 @@ const (
|
||||
FROM information_schema.tables t
|
||||
JOIN information_schema.columns c USING (table_schema,table_name)
|
||||
WHERE c.extra = 'auto_increment' AND t.auto_increment IS NOT NULL
|
||||
`
|
||||
innoDBMetricsQuery = `
|
||||
SELECT NAME, COUNT
|
||||
FROM information_schema.INNODB_METRICS
|
||||
WHERE status='enabled'
|
||||
`
|
||||
perfTableIOWaitsQuery = `
|
||||
SELECT OBJECT_SCHEMA, OBJECT_NAME, COUNT_FETCH, COUNT_INSERT, COUNT_UPDATE, COUNT_DELETE,
|
||||
@@ -530,7 +547,6 @@ const (
|
||||
table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'performance_schema' AND table_name = ?
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
@@ -582,6 +598,13 @@ func (m *Mysql) gatherServer(serv string, acc telegraf.Accumulator) error {
|
||||
}
|
||||
}
|
||||
|
||||
if m.GatherUserStatistics {
|
||||
err = m.GatherUserStatisticsStatuses(db, serv, acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.GatherSlaveStatus {
|
||||
err = m.gatherSlaveStatuses(db, serv, acc)
|
||||
if err != nil {
|
||||
@@ -596,6 +619,13 @@ func (m *Mysql) gatherServer(serv string, acc telegraf.Accumulator) error {
|
||||
}
|
||||
}
|
||||
|
||||
if m.GatherInnoDBMetrics {
|
||||
err = m.gatherInnoDBMetrics(db, serv, acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.GatherTableIOWaits {
|
||||
err = m.gatherPerfTableIOWaits(db, serv, acc)
|
||||
if err != nil {
|
||||
@@ -669,6 +699,11 @@ func (m *Mysql) gatherGlobalVariables(db *sql.DB, serv string, acc telegraf.Accu
|
||||
return err
|
||||
}
|
||||
key = strings.ToLower(key)
|
||||
// parse mysql version and put into field and tag
|
||||
if strings.Contains(key, "version") {
|
||||
fields[key] = string(val)
|
||||
tags[key] = string(val)
|
||||
}
|
||||
// parse value, if it is numeric then save, otherwise ignore
|
||||
if floatVal, ok := parseValue(val); ok {
|
||||
fields[key] = floatVal
|
||||
@@ -854,6 +889,12 @@ func (m *Mysql) gatherGlobalStatuses(db *sql.DB, serv string, acc telegraf.Accum
|
||||
return err
|
||||
}
|
||||
fields["syncs"] = i
|
||||
case "Uptime":
|
||||
i, err := strconv.ParseInt(string(val.([]byte)), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fields["uptime"] = i
|
||||
}
|
||||
}
|
||||
// Send any remaining fields
|
||||
@@ -884,6 +925,74 @@ func (m *Mysql) gatherGlobalStatuses(db *sql.DB, serv string, acc telegraf.Accum
|
||||
}
|
||||
}
|
||||
|
||||
// gather connection metrics from user_statistics for each user
|
||||
if m.GatherUserStatistics {
|
||||
conn_rows, err := db.Query("select user, total_connections, concurrent_connections, connected_time, busy_time, cpu_time, bytes_received, bytes_sent, binlog_bytes_written, rows_fetched, rows_updated, table_rows_read, select_commands, update_commands, other_commands, commit_transactions, rollback_transactions, denied_connections, lost_connections, access_denied, empty_queries, total_ssl_connections FROM INFORMATION_SCHEMA.USER_STATISTICS GROUP BY user")
|
||||
|
||||
for conn_rows.Next() {
|
||||
var user string
|
||||
var total_connections int64
|
||||
var concurrent_connections int64
|
||||
var connected_time int64
|
||||
var busy_time int64
|
||||
var cpu_time int64
|
||||
var bytes_received int64
|
||||
var bytes_sent int64
|
||||
var binlog_bytes_written int64
|
||||
var rows_fetched int64
|
||||
var rows_updated int64
|
||||
var table_rows_read int64
|
||||
var select_commands int64
|
||||
var update_commands int64
|
||||
var other_commands int64
|
||||
var commit_transactions int64
|
||||
var rollback_transactions int64
|
||||
var denied_connections int64
|
||||
var lost_connections int64
|
||||
var access_denied int64
|
||||
var empty_queries int64
|
||||
var total_ssl_connections int64
|
||||
|
||||
err = conn_rows.Scan(&user, &total_connections, &concurrent_connections,
|
||||
&connected_time, &busy_time, &cpu_time, &bytes_received, &bytes_sent, &binlog_bytes_written,
|
||||
&rows_fetched, &rows_updated, &table_rows_read, &select_commands, &update_commands, &other_commands,
|
||||
&commit_transactions, &rollback_transactions, &denied_connections, &lost_connections, &access_denied,
|
||||
&empty_queries, &total_ssl_connections,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags := map[string]string{"server": servtag, "user": user}
|
||||
fields := map[string]interface{}{
|
||||
"total_connections": total_connections,
|
||||
"concurrent_connections": concurrent_connections,
|
||||
"connected_time": connected_time,
|
||||
"busy_time": busy_time,
|
||||
"cpu_time": cpu_time,
|
||||
"bytes_received": bytes_received,
|
||||
"bytes_sent": bytes_sent,
|
||||
"binlog_bytes_written": binlog_bytes_written,
|
||||
"rows_fetched": rows_fetched,
|
||||
"rows_updated": rows_updated,
|
||||
"table_rows_read": table_rows_read,
|
||||
"select_commands": select_commands,
|
||||
"update_commands": update_commands,
|
||||
"other_commands": other_commands,
|
||||
"commit_transactions": commit_transactions,
|
||||
"rollback_transactions": rollback_transactions,
|
||||
"denied_connections": denied_connections,
|
||||
"lost_connections": lost_connections,
|
||||
"access_denied": access_denied,
|
||||
"empty_queries": empty_queries,
|
||||
"total_ssl_connections": total_ssl_connections,
|
||||
}
|
||||
|
||||
acc.AddFields("mysql_user_stats", fields, tags)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -932,6 +1041,83 @@ func (m *Mysql) GatherProcessListStatuses(db *sql.DB, serv string, acc telegraf.
|
||||
return nil
|
||||
}
|
||||
|
||||
// GatherUserStatistics can be used to collect metrics on each running command
|
||||
// and its state with its running count
|
||||
func (m *Mysql) GatherUserStatisticsStatuses(db *sql.DB, serv string, acc telegraf.Accumulator) error {
|
||||
// run query
|
||||
rows, err := db.Query(infoSchemaUserStatisticsQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
var (
|
||||
user string
|
||||
total_connections int64
|
||||
concurrent_connections int64
|
||||
connected_time int64
|
||||
busy_time int64
|
||||
cpu_time int64
|
||||
bytes_received int64
|
||||
bytes_sent int64
|
||||
binlog_bytes_written int64
|
||||
rows_fetched int64
|
||||
rows_updated int64
|
||||
table_rows_read int64
|
||||
select_commands int64
|
||||
update_commands int64
|
||||
other_commands int64
|
||||
commit_transactions int64
|
||||
rollback_transactions int64
|
||||
denied_connections int64
|
||||
lost_connections int64
|
||||
access_denied int64
|
||||
empty_queries int64
|
||||
total_ssl_connections int64
|
||||
count uint32
|
||||
)
|
||||
|
||||
servtag := getDSNTag(serv)
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&user, &total_connections, &concurrent_connections,
|
||||
&connected_time, &busy_time, &cpu_time, &bytes_received, &bytes_sent, &binlog_bytes_written,
|
||||
&rows_fetched, &rows_updated, &table_rows_read, &select_commands, &update_commands, &other_commands,
|
||||
&commit_transactions, &rollback_transactions, &denied_connections, &lost_connections, &access_denied,
|
||||
&empty_queries, &total_ssl_connections, &count,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags := map[string]string{"server": servtag, "user": user}
|
||||
fields := map[string]interface{}{
|
||||
|
||||
"total_connections": total_connections,
|
||||
"concurrent_connections": concurrent_connections,
|
||||
"connected_time": connected_time,
|
||||
"busy_time": busy_time,
|
||||
"cpu_time": cpu_time,
|
||||
"bytes_received": bytes_received,
|
||||
"bytes_sent": bytes_sent,
|
||||
"binlog_bytes_written": binlog_bytes_written,
|
||||
"rows_fetched": rows_fetched,
|
||||
"rows_updated": rows_updated,
|
||||
"table_rows_read": table_rows_read,
|
||||
"select_commands": select_commands,
|
||||
"update_commands": update_commands,
|
||||
"other_commands": other_commands,
|
||||
"commit_transactions": commit_transactions,
|
||||
"rollback_transactions": rollback_transactions,
|
||||
"denied_connections": denied_connections,
|
||||
"lost_connections": lost_connections,
|
||||
"access_denied": access_denied,
|
||||
"empty_queries": empty_queries,
|
||||
"total_ssl_connections": total_ssl_connections,
|
||||
}
|
||||
acc.AddFields("mysql_user_stats", fields, tags)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// gatherPerfTableIOWaits can be used to get total count and time
|
||||
// of I/O wait event for each table and process
|
||||
func (m *Mysql) gatherPerfTableIOWaits(db *sql.DB, serv string, acc telegraf.Accumulator) error {
|
||||
@@ -1069,6 +1255,45 @@ func (m *Mysql) gatherInfoSchemaAutoIncStatuses(db *sql.DB, serv string, acc tel
|
||||
return nil
|
||||
}
|
||||
|
||||
// gatherInnoDBMetrics can be used to fetch enabled metrics from
|
||||
// information_schema.INNODB_METRICS
|
||||
func (m *Mysql) gatherInnoDBMetrics(db *sql.DB, serv string, acc telegraf.Accumulator) error {
|
||||
// run query
|
||||
rows, err := db.Query(innoDBMetricsQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var key string
|
||||
var val sql.RawBytes
|
||||
|
||||
// parse DSN and save server tag
|
||||
servtag := getDSNTag(serv)
|
||||
tags := map[string]string{"server": servtag}
|
||||
fields := make(map[string]interface{})
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&key, &val); err != nil {
|
||||
return err
|
||||
}
|
||||
key = strings.ToLower(key)
|
||||
// parse value, if it is numeric then save, otherwise ignore
|
||||
if floatVal, ok := parseValue(val); ok {
|
||||
fields[key] = floatVal
|
||||
}
|
||||
// Send 20 fields at a time
|
||||
if len(fields) >= 20 {
|
||||
acc.AddFields("mysql_innodb", fields, tags)
|
||||
fields = make(map[string]interface{})
|
||||
}
|
||||
}
|
||||
// Send any remaining fields
|
||||
if len(fields) > 0 {
|
||||
acc.AddFields("mysql_innodb", fields, tags)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// gatherPerfTableLockWaits can be used to get
|
||||
// the total number and time for SQL and external lock wait events
|
||||
// for each table and operation
|
||||
|
||||
@@ -136,7 +136,7 @@ func (n *NTPQ) Gather(acc telegraf.Accumulator) error {
|
||||
continue
|
||||
}
|
||||
// seconds in an hour
|
||||
mFields[key] = int64(m) * 360
|
||||
mFields[key] = int64(m) * 3600
|
||||
continue
|
||||
case strings.HasSuffix(when, "d"):
|
||||
m, err := strconv.Atoi(strings.TrimSuffix(fields[index], "d"))
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestHoursNTPQ(t *testing.T) {
|
||||
assert.NoError(t, n.Gather(&acc))
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"when": int64(720),
|
||||
"when": int64(7200),
|
||||
"poll": int64(256),
|
||||
"reach": int64(37),
|
||||
"delay": float64(51.016),
|
||||
|
||||
@@ -4,8 +4,8 @@ This postgresql plugin provides metrics for your postgres database. It currently
|
||||
```
|
||||
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
|
||||
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
|
||||
|
||||
99
plugins/inputs/postgresql/connect.go
Normal file
99
plugins/inputs/postgresql/connect.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx"
|
||||
"github.com/jackc/pgx/stdlib"
|
||||
)
|
||||
|
||||
// pulled from lib/pq
|
||||
// ParseURL no longer needs to be used by clients of this library since supplying a URL as a
|
||||
// connection string to sql.Open() is now supported:
|
||||
//
|
||||
// sql.Open("postgres", "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full")
|
||||
//
|
||||
// It remains exported here for backwards-compatibility.
|
||||
//
|
||||
// ParseURL converts a url to a connection string for driver.Open.
|
||||
// Example:
|
||||
//
|
||||
// "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full"
|
||||
//
|
||||
// converts to:
|
||||
//
|
||||
// "user=bob password=secret host=1.2.3.4 port=5432 dbname=mydb sslmode=verify-full"
|
||||
//
|
||||
// A minimal example:
|
||||
//
|
||||
// "postgres://"
|
||||
//
|
||||
// This will be blank, causing driver.Open to use all of the defaults
|
||||
func ParseURL(uri string) (string, error) {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if u.Scheme != "postgres" && u.Scheme != "postgresql" {
|
||||
return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme)
|
||||
}
|
||||
|
||||
var kvs []string
|
||||
escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`)
|
||||
accrue := func(k, v string) {
|
||||
if v != "" {
|
||||
kvs = append(kvs, k+"="+escaper.Replace(v))
|
||||
}
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
v := u.User.Username()
|
||||
accrue("user", v)
|
||||
|
||||
v, _ = u.User.Password()
|
||||
accrue("password", v)
|
||||
}
|
||||
|
||||
if host, port, err := net.SplitHostPort(u.Host); err != nil {
|
||||
accrue("host", u.Host)
|
||||
} else {
|
||||
accrue("host", host)
|
||||
accrue("port", port)
|
||||
}
|
||||
|
||||
if u.Path != "" {
|
||||
accrue("dbname", u.Path[1:])
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
for k := range q {
|
||||
accrue(k, q.Get(k))
|
||||
}
|
||||
|
||||
sort.Strings(kvs) // Makes testing easier (not a performance concern)
|
||||
return strings.Join(kvs, " "), nil
|
||||
}
|
||||
|
||||
func Connect(address string) (*sql.DB, error) {
|
||||
if strings.HasPrefix(address, "postgres://") || strings.HasPrefix(address, "postgresql://") {
|
||||
return sql.Open("pgx", address)
|
||||
}
|
||||
|
||||
config, err := pgx.ParseDSN(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool, err := pgx.NewConnPool(pgx.ConnPoolConfig{ConnConfig: config})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stdlib.OpenFromConnPool(pool)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package postgresql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -10,8 +9,6 @@ import (
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Postgresql struct {
|
||||
@@ -23,7 +20,7 @@ type Postgresql struct {
|
||||
sanitizedAddress string
|
||||
}
|
||||
|
||||
var ignoredColumns = map[string]bool{"datid": true, "datname": true, "stats_reset": true}
|
||||
var ignoredColumns = map[string]bool{"stats_reset": true}
|
||||
|
||||
var sampleConfig = `
|
||||
## specify address via a url matching:
|
||||
@@ -71,7 +68,7 @@ func (p *Postgresql) Gather(acc telegraf.Accumulator) error {
|
||||
p.Address = localhost
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", p.Address)
|
||||
db, err := Connect(p.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -149,7 +146,7 @@ var passwordKVMatcher, _ = regexp.Compile("password=\\S+ ?")
|
||||
func (p *Postgresql) SanitizedAddress() (_ string, err error) {
|
||||
var canonicalizedAddress string
|
||||
if strings.HasPrefix(p.Address, "postgres://") || strings.HasPrefix(p.Address, "postgresql://") {
|
||||
canonicalizedAddress, err = pq.ParseURL(p.Address)
|
||||
canonicalizedAddress, err = ParseURL(p.Address)
|
||||
if err != nil {
|
||||
return p.sanitizedAddress, err
|
||||
}
|
||||
@@ -185,10 +182,7 @@ func (p *Postgresql) accRow(row scanner, acc telegraf.Accumulator) error {
|
||||
}
|
||||
if columnMap["datname"] != nil {
|
||||
// extract the database name from the column map
|
||||
dbnameChars := (*columnMap["datname"]).([]uint8)
|
||||
for i := 0; i < len(dbnameChars); i++ {
|
||||
dbname.WriteString(string(dbnameChars[i]))
|
||||
}
|
||||
dbname.WriteString((*columnMap["datname"]).(string))
|
||||
} else {
|
||||
dbname.WriteString("postgres")
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
for _, col := range p.AllColumns {
|
||||
availableColumns[col] = true
|
||||
}
|
||||
|
||||
intMetrics := []string{
|
||||
"xact_commit",
|
||||
"xact_rollback",
|
||||
@@ -42,7 +43,6 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
"temp_files",
|
||||
"temp_bytes",
|
||||
"deadlocks",
|
||||
"numbackends",
|
||||
"buffers_alloc",
|
||||
"buffers_backend",
|
||||
"buffers_backend_fsync",
|
||||
@@ -53,9 +53,20 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
"maxwritten_clean",
|
||||
}
|
||||
|
||||
int32Metrics := []string{
|
||||
"numbackends",
|
||||
}
|
||||
|
||||
floatMetrics := []string{
|
||||
"blk_read_time",
|
||||
"blk_write_time",
|
||||
"checkpoint_write_time",
|
||||
"checkpoint_sync_time",
|
||||
}
|
||||
|
||||
stringMetrics := []string{
|
||||
"datname",
|
||||
"datid",
|
||||
}
|
||||
|
||||
metricsCounted := 0
|
||||
@@ -68,6 +79,14 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, metric := range int32Metrics {
|
||||
_, ok := availableColumns[metric]
|
||||
if ok {
|
||||
assert.True(t, acc.HasInt32Field("postgresql", metric))
|
||||
metricsCounted++
|
||||
}
|
||||
}
|
||||
|
||||
for _, metric := range floatMetrics {
|
||||
_, ok := availableColumns[metric]
|
||||
if ok {
|
||||
@@ -76,8 +95,16 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, metric := range stringMetrics {
|
||||
_, ok := availableColumns[metric]
|
||||
if ok {
|
||||
assert.True(t, acc.HasStringField("postgresql", metric))
|
||||
metricsCounted++
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, metricsCounted > 0)
|
||||
//assert.Equal(t, len(availableColumns)-len(p.IgnoredColumns()), metricsCounted)
|
||||
assert.Equal(t, len(availableColumns)-len(p.IgnoredColumns()), metricsCounted)
|
||||
}
|
||||
|
||||
func TestPostgresqlTagsMetricsWithDatabaseName(t *testing.T) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package postgresql_extensible
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
@@ -10,8 +9,7 @@ import (
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/influxdata/telegraf/plugins/inputs/postgresql"
|
||||
)
|
||||
|
||||
type Postgresql struct {
|
||||
@@ -40,7 +38,7 @@ type query []struct {
|
||||
Measurement string
|
||||
}
|
||||
|
||||
var ignoredColumns = map[string]bool{"datid": true, "datname": true, "stats_reset": true}
|
||||
var ignoredColumns = map[string]bool{"stats_reset": true}
|
||||
|
||||
var sampleConfig = `
|
||||
## specify address via a url matching:
|
||||
@@ -126,7 +124,7 @@ func (p *Postgresql) Gather(acc telegraf.Accumulator) error {
|
||||
p.Address = localhost
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", p.Address)
|
||||
db, err := postgresql.Connect(p.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -212,7 +210,7 @@ func (p *Postgresql) SanitizedAddress() (_ string, err error) {
|
||||
}
|
||||
var canonicalizedAddress string
|
||||
if strings.HasPrefix(p.Address, "postgres://") || strings.HasPrefix(p.Address, "postgresql://") {
|
||||
canonicalizedAddress, err = pq.ParseURL(p.Address)
|
||||
canonicalizedAddress, err = postgresql.ParseURL(p.Address)
|
||||
if err != nil {
|
||||
return p.sanitizedAddress, err
|
||||
}
|
||||
@@ -248,10 +246,7 @@ func (p *Postgresql) accRow(meas_name string, row scanner, acc telegraf.Accumula
|
||||
}
|
||||
if columnMap["datname"] != nil {
|
||||
// extract the database name from the column map
|
||||
dbnameChars := (*columnMap["datname"]).([]uint8)
|
||||
for i := 0; i < len(dbnameChars); i++ {
|
||||
dbname.WriteString(string(dbnameChars[i]))
|
||||
}
|
||||
dbname.WriteString((*columnMap["datname"]).(string))
|
||||
} else {
|
||||
dbname.WriteString("postgres")
|
||||
}
|
||||
@@ -275,19 +270,23 @@ COLUMN:
|
||||
if ignore || *val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, tag := range p.AdditionalTags {
|
||||
if col != tag {
|
||||
continue
|
||||
}
|
||||
switch v := (*val).(type) {
|
||||
case string:
|
||||
tags[col] = v
|
||||
case []byte:
|
||||
tags[col] = string(v)
|
||||
case int64:
|
||||
case int64, int32, int:
|
||||
tags[col] = fmt.Sprintf("%d", v)
|
||||
default:
|
||||
log.Println("failed to add additional tag", col)
|
||||
}
|
||||
continue COLUMN
|
||||
}
|
||||
|
||||
if v, ok := (*val).([]byte); ok {
|
||||
fields[col] = string(v)
|
||||
} else {
|
||||
|
||||
@@ -33,6 +33,7 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
for _, col := range p.AllColumns {
|
||||
availableColumns[col] = true
|
||||
}
|
||||
|
||||
intMetrics := []string{
|
||||
"xact_commit",
|
||||
"xact_rollback",
|
||||
@@ -47,6 +48,9 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
"temp_files",
|
||||
"temp_bytes",
|
||||
"deadlocks",
|
||||
}
|
||||
|
||||
int32Metrics := []string{
|
||||
"numbackends",
|
||||
}
|
||||
|
||||
@@ -55,6 +59,11 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
"blk_write_time",
|
||||
}
|
||||
|
||||
stringMetrics := []string{
|
||||
"datname",
|
||||
"datid",
|
||||
}
|
||||
|
||||
metricsCounted := 0
|
||||
|
||||
for _, metric := range intMetrics {
|
||||
@@ -65,6 +74,14 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, metric := range int32Metrics {
|
||||
_, ok := availableColumns[metric]
|
||||
if ok {
|
||||
assert.True(t, acc.HasInt32Field("postgresql", metric))
|
||||
metricsCounted++
|
||||
}
|
||||
}
|
||||
|
||||
for _, metric := range floatMetrics {
|
||||
_, ok := availableColumns[metric]
|
||||
if ok {
|
||||
@@ -73,6 +90,14 @@ func TestPostgresqlGeneratesMetrics(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, metric := range stringMetrics {
|
||||
_, ok := availableColumns[metric]
|
||||
if ok {
|
||||
assert.True(t, acc.HasStringField("postgresql", metric))
|
||||
metricsCounted++
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, metricsCounted > 0)
|
||||
assert.Equal(t, len(availableColumns)-len(p.IgnoredColumns()), metricsCounted)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/process"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
@@ -23,15 +21,12 @@ type Procstat struct {
|
||||
User string
|
||||
PidTag bool
|
||||
|
||||
// pidmap maps a pid to a process object, so we don't recreate every gather
|
||||
pidmap map[int32]*process.Process
|
||||
// tagmap maps a pid to a map of tags for that pid
|
||||
tagmap map[int32]map[string]string
|
||||
}
|
||||
|
||||
func NewProcstat() *Procstat {
|
||||
return &Procstat{
|
||||
pidmap: make(map[int32]*process.Process),
|
||||
tagmap: make(map[int32]map[string]string),
|
||||
}
|
||||
}
|
||||
@@ -67,51 +62,26 @@ func (_ *Procstat) Description() string {
|
||||
}
|
||||
|
||||
func (p *Procstat) Gather(acc telegraf.Accumulator) error {
|
||||
err := p.createProcesses()
|
||||
pids, err := p.getAllPids()
|
||||
if err != nil {
|
||||
log.Printf("E! Error: procstat getting process, exe: [%s] pidfile: [%s] pattern: [%s] user: [%s] %s",
|
||||
p.Exe, p.PidFile, p.Pattern, p.User, err.Error())
|
||||
} else {
|
||||
for pid, proc := range p.pidmap {
|
||||
for _, pid := range pids {
|
||||
if p.PidTag {
|
||||
p.tagmap[pid]["pid"] = fmt.Sprint(pid)
|
||||
}
|
||||
p := NewSpecProcessor(p.ProcessName, p.Prefix, pid, acc, proc, p.tagmap[pid])
|
||||
p.pushMetrics()
|
||||
p := NewSpecProcessor(p.ProcessName, p.Prefix, pid, acc, p.tagmap[pid])
|
||||
err := p.pushMetrics()
|
||||
if err != nil {
|
||||
log.Printf("E! Error: procstat: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Procstat) createProcesses() error {
|
||||
var errstring string
|
||||
var outerr error
|
||||
|
||||
pids, err := p.getAllPids()
|
||||
if err != nil {
|
||||
errstring += err.Error() + " "
|
||||
}
|
||||
|
||||
for _, pid := range pids {
|
||||
_, ok := p.pidmap[pid]
|
||||
if !ok {
|
||||
proc, err := process.NewProcess(pid)
|
||||
if err == nil {
|
||||
p.pidmap[pid] = proc
|
||||
} else {
|
||||
errstring += err.Error() + " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errstring != "" {
|
||||
outerr = fmt.Errorf("%s", errstring)
|
||||
}
|
||||
|
||||
return outerr
|
||||
}
|
||||
|
||||
func (p *Procstat) getAllPids() ([]int32, error) {
|
||||
var pids []int32
|
||||
var err error
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/shirou/gopsutil/process"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -24,7 +23,6 @@ func TestGather(t *testing.T) {
|
||||
p := Procstat{
|
||||
PidFile: file.Name(),
|
||||
Prefix: "foo",
|
||||
pidmap: make(map[int32]*process.Process),
|
||||
tagmap: make(map[int32]map[string]string),
|
||||
}
|
||||
p.Gather(&acc)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package procstat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/process"
|
||||
@@ -9,12 +10,13 @@ import (
|
||||
)
|
||||
|
||||
type SpecProcessor struct {
|
||||
Prefix string
|
||||
pid int32
|
||||
tags map[string]string
|
||||
fields map[string]interface{}
|
||||
acc telegraf.Accumulator
|
||||
proc *process.Process
|
||||
ProcessName string
|
||||
Prefix string
|
||||
pid int32
|
||||
tags map[string]string
|
||||
fields map[string]interface{}
|
||||
acc telegraf.Accumulator
|
||||
proc *process.Process
|
||||
}
|
||||
|
||||
func NewSpecProcessor(
|
||||
@@ -22,29 +24,35 @@ func NewSpecProcessor(
|
||||
prefix string,
|
||||
pid int32,
|
||||
acc telegraf.Accumulator,
|
||||
p *process.Process,
|
||||
tags map[string]string,
|
||||
) *SpecProcessor {
|
||||
if processName != "" {
|
||||
tags["process_name"] = processName
|
||||
} else {
|
||||
name, err := p.Name()
|
||||
if err == nil {
|
||||
tags["process_name"] = name
|
||||
}
|
||||
}
|
||||
return &SpecProcessor{
|
||||
Prefix: prefix,
|
||||
pid: pid,
|
||||
tags: tags,
|
||||
fields: make(map[string]interface{}),
|
||||
acc: acc,
|
||||
proc: p,
|
||||
ProcessName: processName,
|
||||
Prefix: prefix,
|
||||
pid: pid,
|
||||
tags: tags,
|
||||
fields: make(map[string]interface{}),
|
||||
acc: acc,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SpecProcessor) pushMetrics() {
|
||||
func (p *SpecProcessor) pushMetrics() error {
|
||||
var prefix string
|
||||
proc, err := process.NewProcess(p.pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open process with pid '%d'. Error: '%s'",
|
||||
p.pid, err)
|
||||
}
|
||||
p.proc = proc
|
||||
if p.ProcessName != "" {
|
||||
p.tags["process_name"] = p.ProcessName
|
||||
} else {
|
||||
name, err := p.proc.Name()
|
||||
if err == nil {
|
||||
p.tags["process_name"] = name
|
||||
}
|
||||
}
|
||||
|
||||
if p.Prefix != "" {
|
||||
prefix = p.Prefix + "_"
|
||||
}
|
||||
@@ -107,4 +115,5 @@ func (p *SpecProcessor) pushMetrics() {
|
||||
}
|
||||
|
||||
p.acc.AddFields("procstat", fields, p.tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -433,9 +433,7 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
|
||||
if err != nil {
|
||||
return nil, Errorf(err, "converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name)
|
||||
}
|
||||
if fvs, ok := fv.(string); !ok || fvs != "" {
|
||||
ifv[""] = fv
|
||||
}
|
||||
ifv[""] = fv
|
||||
}
|
||||
} else {
|
||||
err := gs.Walk(oid, func(ent gosnmp.SnmpPDU) error {
|
||||
@@ -456,9 +454,7 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
|
||||
if err != nil {
|
||||
return Errorf(err, "converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name)
|
||||
}
|
||||
if fvs, ok := fv.(string); !ok || fvs != "" {
|
||||
ifv[idx] = fv
|
||||
}
|
||||
ifv[idx] = fv
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -476,14 +472,17 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
|
||||
rtr.Fields = map[string]interface{}{}
|
||||
rows[i] = rtr
|
||||
}
|
||||
if f.IsTag {
|
||||
if vs, ok := v.(string); ok {
|
||||
rtr.Tags[f.Name] = vs
|
||||
// don't add an empty string
|
||||
if vs, ok := v.(string); !ok || vs != "" {
|
||||
if f.IsTag {
|
||||
if ok {
|
||||
rtr.Tags[f.Name] = vs
|
||||
} else {
|
||||
rtr.Tags[f.Name] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
} else {
|
||||
rtr.Tags[f.Name] = fmt.Sprintf("%v", v)
|
||||
rtr.Fields[f.Name] = v
|
||||
}
|
||||
} else {
|
||||
rtr.Fields[f.Name] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,10 +493,6 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
|
||||
Rows: make([]RTableRow, 0, len(rows)),
|
||||
}
|
||||
for _, r := range rows {
|
||||
if len(r.Tags) < tagCount {
|
||||
// don't add rows which are missing tags, as without tags you can't filter
|
||||
continue
|
||||
}
|
||||
rt.Rows = append(rt.Rows, r)
|
||||
}
|
||||
return &rt, nil
|
||||
|
||||
@@ -457,9 +457,24 @@ func TestTableBuild_walk(t *testing.T) {
|
||||
"myfield4": 22,
|
||||
},
|
||||
}
|
||||
assert.Len(t, tb.Rows, 2)
|
||||
rtr3 := RTableRow{
|
||||
Tags: map[string]string{},
|
||||
Fields: map[string]interface{}{
|
||||
"myfield2": 0,
|
||||
"myfield3": float64(0.0),
|
||||
},
|
||||
}
|
||||
rtr4 := RTableRow{
|
||||
Tags: map[string]string{},
|
||||
Fields: map[string]interface{}{
|
||||
"myfield3": float64(9.999),
|
||||
},
|
||||
}
|
||||
assert.Len(t, tb.Rows, 4)
|
||||
assert.Contains(t, tb.Rows, rtr1)
|
||||
assert.Contains(t, tb.Rows, rtr2)
|
||||
assert.Contains(t, tb.Rows, rtr3)
|
||||
assert.Contains(t, tb.Rows, rtr4)
|
||||
}
|
||||
|
||||
func TestTableBuild_noWalk(t *testing.T) {
|
||||
|
||||
240
plugins/inputs/socket_listener/socket_listener.go
Normal file
240
plugins/inputs/socket_listener/socket_listener.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package socket_listener
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
"github.com/influxdata/telegraf/plugins/parsers"
|
||||
)
|
||||
|
||||
type setReadBufferer interface {
|
||||
SetReadBuffer(bytes int) error
|
||||
}
|
||||
|
||||
type streamSocketListener struct {
|
||||
net.Listener
|
||||
*SocketListener
|
||||
|
||||
connections map[string]net.Conn
|
||||
connectionsMtx sync.Mutex
|
||||
}
|
||||
|
||||
func (ssl *streamSocketListener) listen() {
|
||||
ssl.connections = map[string]net.Conn{}
|
||||
|
||||
for {
|
||||
c, err := ssl.Accept()
|
||||
if err != nil {
|
||||
ssl.AddError(err)
|
||||
break
|
||||
}
|
||||
|
||||
ssl.connectionsMtx.Lock()
|
||||
if ssl.MaxConnections > 0 && len(ssl.connections) >= ssl.MaxConnections {
|
||||
ssl.connectionsMtx.Unlock()
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
ssl.connections[c.RemoteAddr().String()] = c
|
||||
ssl.connectionsMtx.Unlock()
|
||||
go ssl.read(c)
|
||||
}
|
||||
|
||||
ssl.connectionsMtx.Lock()
|
||||
for _, c := range ssl.connections {
|
||||
c.Close()
|
||||
}
|
||||
ssl.connectionsMtx.Unlock()
|
||||
}
|
||||
|
||||
func (ssl *streamSocketListener) removeConnection(c net.Conn) {
|
||||
ssl.connectionsMtx.Lock()
|
||||
delete(ssl.connections, c.RemoteAddr().String())
|
||||
ssl.connectionsMtx.Unlock()
|
||||
}
|
||||
|
||||
func (ssl *streamSocketListener) read(c net.Conn) {
|
||||
defer ssl.removeConnection(c)
|
||||
defer c.Close()
|
||||
|
||||
scnr := bufio.NewScanner(c)
|
||||
for scnr.Scan() {
|
||||
metrics, err := ssl.Parse(scnr.Bytes())
|
||||
if err != nil {
|
||||
ssl.AddError(fmt.Errorf("unable to parse incoming line"))
|
||||
//TODO rate limit
|
||||
continue
|
||||
}
|
||||
for _, m := range metrics {
|
||||
ssl.AddFields(m.Name(), m.Fields(), m.Tags(), m.Time())
|
||||
}
|
||||
}
|
||||
|
||||
if err := scnr.Err(); err != nil {
|
||||
ssl.AddError(err)
|
||||
}
|
||||
}
|
||||
|
||||
type packetSocketListener struct {
|
||||
net.PacketConn
|
||||
*SocketListener
|
||||
}
|
||||
|
||||
func (psl *packetSocketListener) listen() {
|
||||
buf := make([]byte, 64*1024) // 64kb - maximum size of IP packet
|
||||
for {
|
||||
n, _, err := psl.ReadFrom(buf)
|
||||
if err != nil {
|
||||
psl.AddError(err)
|
||||
break
|
||||
}
|
||||
|
||||
metrics, err := psl.Parse(buf[:n])
|
||||
if err != nil {
|
||||
psl.AddError(fmt.Errorf("unable to parse incoming packet"))
|
||||
//TODO rate limit
|
||||
continue
|
||||
}
|
||||
for _, m := range metrics {
|
||||
psl.AddFields(m.Name(), m.Fields(), m.Tags(), m.Time())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SocketListener struct {
|
||||
ServiceAddress string
|
||||
MaxConnections int
|
||||
ReadBufferSize int
|
||||
|
||||
parsers.Parser
|
||||
telegraf.Accumulator
|
||||
io.Closer
|
||||
}
|
||||
|
||||
func (sl *SocketListener) Description() string {
|
||||
return "Generic socket listener capable of handling multiple socket types."
|
||||
}
|
||||
|
||||
func (sl *SocketListener) SampleConfig() string {
|
||||
return `
|
||||
## URL to listen on
|
||||
# service_address = "tcp://:8094"
|
||||
# service_address = "tcp://127.0.0.1:http"
|
||||
# service_address = "tcp4://:8094"
|
||||
# service_address = "tcp6://:8094"
|
||||
# service_address = "tcp6://[2001:db8::1]:8094"
|
||||
# service_address = "udp://:8094"
|
||||
# service_address = "udp4://:8094"
|
||||
# service_address = "udp6://:8094"
|
||||
# service_address = "unix:///tmp/telegraf.sock"
|
||||
# service_address = "unixgram:///tmp/telegraf.sock"
|
||||
|
||||
## Maximum number of concurrent connections.
|
||||
## Only applies to stream sockets (e.g. TCP).
|
||||
## 0 (default) is unlimited.
|
||||
# max_connections = 1024
|
||||
|
||||
## Maximum socket buffer size in bytes.
|
||||
## For stream sockets, once the buffer fills up, the sender will start backing up.
|
||||
## For datagram sockets, once the buffer fills up, metrics will start dropping.
|
||||
## Defaults to the OS default.
|
||||
# read_buffer_size = 65535
|
||||
|
||||
## Data format to consume.
|
||||
## Each data format has it's own unique set of configuration options, read
|
||||
## more about them here:
|
||||
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md
|
||||
# data_format = "influx"
|
||||
`
|
||||
}
|
||||
|
||||
func (sl *SocketListener) Gather(_ telegraf.Accumulator) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sl *SocketListener) SetParser(parser parsers.Parser) {
|
||||
sl.Parser = parser
|
||||
}
|
||||
|
||||
func (sl *SocketListener) Start(acc telegraf.Accumulator) error {
|
||||
sl.Accumulator = acc
|
||||
spl := strings.SplitN(sl.ServiceAddress, "://", 2)
|
||||
if len(spl) != 2 {
|
||||
return fmt.Errorf("invalid service address: %s", sl.ServiceAddress)
|
||||
}
|
||||
|
||||
switch spl[0] {
|
||||
case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
|
||||
l, err := net.Listen(spl[0], spl[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sl.ReadBufferSize > 0 {
|
||||
if srb, ok := l.(setReadBufferer); ok {
|
||||
srb.SetReadBuffer(sl.ReadBufferSize)
|
||||
} else {
|
||||
log.Printf("W! Unable to set read buffer on a %s socket", spl[0])
|
||||
}
|
||||
}
|
||||
|
||||
ssl := &streamSocketListener{
|
||||
Listener: l,
|
||||
SocketListener: sl,
|
||||
}
|
||||
|
||||
sl.Closer = ssl
|
||||
go ssl.listen()
|
||||
case "udp", "udp4", "udp6", "ip", "ip4", "ip6", "unixgram":
|
||||
pc, err := net.ListenPacket(spl[0], spl[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sl.ReadBufferSize > 0 {
|
||||
if srb, ok := pc.(setReadBufferer); ok {
|
||||
srb.SetReadBuffer(sl.ReadBufferSize)
|
||||
} else {
|
||||
log.Printf("W! Unable to set read buffer on a %s socket", spl[0])
|
||||
}
|
||||
}
|
||||
|
||||
psl := &packetSocketListener{
|
||||
PacketConn: pc,
|
||||
SocketListener: sl,
|
||||
}
|
||||
|
||||
sl.Closer = psl
|
||||
go psl.listen()
|
||||
default:
|
||||
return fmt.Errorf("unknown protocol '%s' in '%s'", spl[0], sl.ServiceAddress)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sl *SocketListener) Stop() {
|
||||
if sl.Closer != nil {
|
||||
sl.Close()
|
||||
sl.Closer = nil
|
||||
}
|
||||
}
|
||||
|
||||
func newSocketListener() *SocketListener {
|
||||
parser, _ := parsers.NewInfluxParser()
|
||||
|
||||
return &SocketListener{
|
||||
Parser: parser,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("socket_listener", func() telegraf.Input { return newSocketListener() })
|
||||
}
|
||||
122
plugins/inputs/socket_listener/socket_listener_test.go
Normal file
122
plugins/inputs/socket_listener/socket_listener_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package socket_listener
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSocketListener_tcp(t *testing.T) {
|
||||
sl := newSocketListener()
|
||||
sl.ServiceAddress = "tcp://127.0.0.1:0"
|
||||
|
||||
acc := &testutil.Accumulator{}
|
||||
err := sl.Start(acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := net.Dial("tcp", sl.Closer.(net.Listener).Addr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
testSocketListener(t, sl, client)
|
||||
}
|
||||
|
||||
func TestSocketListener_udp(t *testing.T) {
|
||||
sl := newSocketListener()
|
||||
sl.ServiceAddress = "udp://127.0.0.1:0"
|
||||
|
||||
acc := &testutil.Accumulator{}
|
||||
err := sl.Start(acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := net.Dial("udp", sl.Closer.(net.PacketConn).LocalAddr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
testSocketListener(t, sl, client)
|
||||
}
|
||||
|
||||
func TestSocketListener_unix(t *testing.T) {
|
||||
defer os.Remove("/tmp/telegraf_test.sock")
|
||||
sl := newSocketListener()
|
||||
sl.ServiceAddress = "unix:///tmp/telegraf_test.sock"
|
||||
|
||||
acc := &testutil.Accumulator{}
|
||||
err := sl.Start(acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := net.Dial("unix", "/tmp/telegraf_test.sock")
|
||||
require.NoError(t, err)
|
||||
|
||||
testSocketListener(t, sl, client)
|
||||
}
|
||||
|
||||
func TestSocketListener_unixgram(t *testing.T) {
|
||||
defer os.Remove("/tmp/telegraf_test.sock")
|
||||
sl := newSocketListener()
|
||||
sl.ServiceAddress = "unixgram:///tmp/telegraf_test.sock"
|
||||
|
||||
acc := &testutil.Accumulator{}
|
||||
err := sl.Start(acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := net.Dial("unixgram", "/tmp/telegraf_test.sock")
|
||||
require.NoError(t, err)
|
||||
|
||||
testSocketListener(t, sl, client)
|
||||
}
|
||||
|
||||
func testSocketListener(t *testing.T, sl *SocketListener, client net.Conn) {
|
||||
mstr12 := "test,foo=bar v=1i 123456789\ntest,foo=baz v=2i 123456790\n"
|
||||
mstr3 := "test,foo=zab v=3i 123456791"
|
||||
client.Write([]byte(mstr12))
|
||||
client.Write([]byte(mstr3))
|
||||
if _, ok := client.(net.Conn); ok {
|
||||
// stream connection. needs trailing newline to terminate mstr3
|
||||
client.Write([]byte{'\n'})
|
||||
}
|
||||
|
||||
acc := sl.Accumulator.(*testutil.Accumulator)
|
||||
|
||||
acc.Lock()
|
||||
if len(acc.Metrics) < 1 {
|
||||
acc.Wait()
|
||||
}
|
||||
require.True(t, len(acc.Metrics) >= 1)
|
||||
m := acc.Metrics[0]
|
||||
acc.Unlock()
|
||||
|
||||
assert.Equal(t, "test", m.Measurement)
|
||||
assert.Equal(t, map[string]string{"foo": "bar"}, m.Tags)
|
||||
assert.Equal(t, map[string]interface{}{"v": int64(1)}, m.Fields)
|
||||
assert.True(t, time.Unix(0, 123456789).Equal(m.Time))
|
||||
|
||||
acc.Lock()
|
||||
if len(acc.Metrics) < 2 {
|
||||
acc.Wait()
|
||||
}
|
||||
require.True(t, len(acc.Metrics) >= 2)
|
||||
m = acc.Metrics[1]
|
||||
acc.Unlock()
|
||||
|
||||
assert.Equal(t, "test", m.Measurement)
|
||||
assert.Equal(t, map[string]string{"foo": "baz"}, m.Tags)
|
||||
assert.Equal(t, map[string]interface{}{"v": int64(2)}, m.Fields)
|
||||
assert.True(t, time.Unix(0, 123456790).Equal(m.Time))
|
||||
|
||||
acc.Lock()
|
||||
if len(acc.Metrics) < 3 {
|
||||
acc.Wait()
|
||||
}
|
||||
require.True(t, len(acc.Metrics) >= 3)
|
||||
m = acc.Metrics[2]
|
||||
acc.Unlock()
|
||||
|
||||
assert.Equal(t, "test", m.Measurement)
|
||||
assert.Equal(t, map[string]string{"foo": "zab"}, m.Tags)
|
||||
assert.Equal(t, map[string]interface{}{"v": int64(3)}, m.Fields)
|
||||
assert.True(t, time.Unix(0, 123456791).Equal(m.Time))
|
||||
}
|
||||
@@ -402,8 +402,8 @@ IF OBJECT_ID('tempdb..#baseline') IS NOT NULL
|
||||
DROP TABLE #baseline;
|
||||
SELECT
|
||||
DB_NAME(mf.database_id) AS database_name ,
|
||||
mf.size as database_size_8k_pages,
|
||||
mf.max_size as database_max_size_8k_pages,
|
||||
CAST(mf.size AS BIGINT) as database_size_8k_pages,
|
||||
CAST(mf.max_size AS BIGINT) as database_max_size_8k_pages,
|
||||
size_on_disk_bytes ,
|
||||
type_desc as datafile_type,
|
||||
GETDATE() AS baselineDate
|
||||
|
||||
@@ -51,3 +51,57 @@ In this case, the host's root volume should be mounted into the container and th
|
||||
> disk,fstype=autofs,path=/net free=0i,inodes_free=0i,inodes_total=0i,inodes_used=0i,total=0i,used=0i,used_percent=0 1453832006274157077
|
||||
> disk,fstype=autofs,path=/home free=0i,inodes_free=0i,inodes_total=0i,inodes_used=0i,total=0i,used=0i,used_percent=0 1453832006274169688
|
||||
```
|
||||
|
||||
|
||||
# DiskIO Input Plugin
|
||||
|
||||
The diskio input plugin gathers metrics about disk traffic and timing.
|
||||
|
||||
### Configuration:
|
||||
|
||||
```
|
||||
# Read metrics about disk IO by device
|
||||
[[inputs.diskio]]
|
||||
## By default, telegraf will gather stats for all devices including
|
||||
## disk partitions.
|
||||
## Setting devices will restrict the stats to the specified devices.
|
||||
# devices = ["sda", "sdb"]
|
||||
## Uncomment the following line if you need disk serial numbers.
|
||||
# skip_serial_number = false
|
||||
```
|
||||
|
||||
Data collection is based on github.com/shirou/gopsutil. This package handles platform dependencies and converts all timing information to milliseconds.
|
||||
|
||||
|
||||
### Measurements & Fields:
|
||||
|
||||
- diskio
|
||||
- reads (integer, counter)
|
||||
- writes (integer, counter)
|
||||
- read_bytes (integer, bytes)
|
||||
- write_bytes (integer, bytes)
|
||||
- read_time (integer, milliseconds)
|
||||
- write_time (integer, milliseconds)
|
||||
- io_time (integer, milliseconds)
|
||||
- iops_in_progress (integer, counter) (since #2037, not yet in STABLE)
|
||||
|
||||
### Tags:
|
||||
|
||||
- All measurements have the following tags:
|
||||
- name (device name)
|
||||
- If configured to use serial numbers (default: disabled):
|
||||
- serial (device serial number)
|
||||
|
||||
### Example Output:
|
||||
|
||||
```
|
||||
% telegraf -config ~/.telegraf/telegraf.conf -input-filter diskio -test
|
||||
* Plugin: inputs.diskio, Collection 1
|
||||
> diskio,name=mmcblk1p2 io_time=244i,read_bytes=966656i,read_time=276i,reads=128i,write_bytes=0i,write_time=0i,writes=0i 1484916036000000000
|
||||
> diskio,name=mmcblk1boot1 io_time=264i,read_bytes=90112i,read_time=264i,reads=22i,write_bytes=0i,write_time=0i,writes=0i 1484916036000000000
|
||||
> diskio,name=mmcblk1boot0 io_time=212i,read_bytes=90112i,read_time=212i,reads=22i,write_bytes=0i,write_time=0i,writes=0i 1484916036000000000
|
||||
> diskio,name=mmcblk0 io_time=1855380i,read_bytes=135861248i,read_time=58484i,reads=4081i,write_bytes=364068864i,write_time=7128792i,writes=18019i 1484916036000000000
|
||||
> diskio,name=mmcblk0p1 io_time=1855256i,read_bytes=134915072i,read_time=58256i,reads=3958i,write_bytes=364068864i,write_time=7128792i,writes=18019i 1484916036000000000
|
||||
> diskio,name=mmcblk1 io_time=384i,read_bytes=2633728i,read_time=728i,reads=323i,write_bytes=0i,write_time=0i,writes=0i 1484916036000000000
|
||||
> diskio,name=mmcblk1p1 io_time=216i,read_bytes=860160i,read_time=288i,reads=106i,write_bytes=0i,write_time=0i,writes=0i 1484916036000000000
|
||||
```
|
||||
|
||||
@@ -2,6 +2,7 @@ package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
@@ -29,7 +30,7 @@ var diskSampleConfig = `
|
||||
|
||||
## Ignore some mountpoints by filesystem type. For example (dev)tmpfs (usually
|
||||
## present on /run, /var/run, /dev/shm or /dev).
|
||||
ignore_fs = ["tmpfs", "devtmpfs"]
|
||||
ignore_fs = ["tmpfs", "devtmpfs", "devfs"]
|
||||
`
|
||||
|
||||
func (_ *DiskStats) SampleConfig() string {
|
||||
@@ -82,7 +83,11 @@ type DiskIOStats struct {
|
||||
ps PS
|
||||
|
||||
Devices []string
|
||||
DeviceTags []string
|
||||
NameTemplates []string
|
||||
SkipSerialNumber bool
|
||||
|
||||
infoCache map[string]diskInfoCache
|
||||
}
|
||||
|
||||
func (_ *DiskIOStats) Description() string {
|
||||
@@ -96,6 +101,23 @@ var diskIoSampleConfig = `
|
||||
# devices = ["sda", "sdb"]
|
||||
## Uncomment the following line if you need disk serial numbers.
|
||||
# skip_serial_number = false
|
||||
#
|
||||
## On systems which support it, device metadata can be added in the form of
|
||||
## tags.
|
||||
## Currently only Linux is supported via udev properties. You can view
|
||||
## available properties for a device by running:
|
||||
## 'udevadm info -q property -n /dev/sda'
|
||||
# device_tags = ["ID_FS_TYPE", "ID_FS_USAGE"]
|
||||
#
|
||||
## Using the same metadata source as device_tags, you can also customize the
|
||||
## name of the device via templates.
|
||||
## The 'name_templates' parameter is a list of templates to try and apply to
|
||||
## the device. The template may contain variables in the form of '$PROPERTY' or
|
||||
## '${PROPERTY}'. The first template which does not contain any variables not
|
||||
## present for the device is used as the device name tag.
|
||||
## The typical use case is for LVM volumes, to get the VG/LV name instead of
|
||||
## the near-meaningless DM-0 name.
|
||||
# name_templates = ["$ID_FS_LABEL","$DM_VG_NAME/$DM_LV_NAME"]
|
||||
`
|
||||
|
||||
func (_ *DiskIOStats) SampleConfig() string {
|
||||
@@ -123,7 +145,10 @@ func (s *DiskIOStats) Gather(acc telegraf.Accumulator) error {
|
||||
continue
|
||||
}
|
||||
tags := map[string]string{}
|
||||
tags["name"] = io.Name
|
||||
tags["name"] = s.diskName(io.Name)
|
||||
for t, v := range s.diskTags(io.Name) {
|
||||
tags[t] = v
|
||||
}
|
||||
if !s.SkipSerialNumber {
|
||||
if len(io.SerialNumber) != 0 {
|
||||
tags["serial"] = io.SerialNumber
|
||||
@@ -148,6 +173,64 @@ func (s *DiskIOStats) Gather(acc telegraf.Accumulator) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var varRegex = regexp.MustCompile(`\$(?:\w+|\{\w+\})`)
|
||||
|
||||
func (s *DiskIOStats) diskName(devName string) string {
|
||||
di, err := s.diskInfo(devName)
|
||||
if err != nil {
|
||||
// discard error :-(
|
||||
// We can't return error because it's non-fatal to the Gather().
|
||||
// And we have no logger, so we can't log it.
|
||||
return devName
|
||||
}
|
||||
if di == nil {
|
||||
return devName
|
||||
}
|
||||
|
||||
for _, nt := range s.NameTemplates {
|
||||
miss := false
|
||||
name := varRegex.ReplaceAllStringFunc(nt, func(sub string) string {
|
||||
sub = sub[1:] // strip leading '$'
|
||||
if sub[0] == '{' {
|
||||
sub = sub[1 : len(sub)-1] // strip leading & trailing '{' '}'
|
||||
}
|
||||
if v, ok := di[sub]; ok {
|
||||
return v
|
||||
}
|
||||
miss = true
|
||||
return ""
|
||||
})
|
||||
|
||||
if !miss {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
return devName
|
||||
}
|
||||
|
||||
func (s *DiskIOStats) diskTags(devName string) map[string]string {
|
||||
di, err := s.diskInfo(devName)
|
||||
if err != nil {
|
||||
// discard error :-(
|
||||
// We can't return error because it's non-fatal to the Gather().
|
||||
// And we have no logger, so we can't log it.
|
||||
return nil
|
||||
}
|
||||
if di == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tags := map[string]string{}
|
||||
for _, dt := range s.DeviceTags {
|
||||
if v, ok := di[dt]; ok {
|
||||
tags[dt] = v
|
||||
}
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("disk", func() telegraf.Input {
|
||||
return &DiskStats{ps: &systemPS{}}
|
||||
|
||||
66
plugins/inputs/system/disk_linux.go
Normal file
66
plugins/inputs/system/disk_linux.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type diskInfoCache struct {
|
||||
stat syscall.Stat_t
|
||||
values map[string]string
|
||||
}
|
||||
|
||||
var udevPath = "/run/udev/data"
|
||||
|
||||
func (s *DiskIOStats) diskInfo(devName string) (map[string]string, error) {
|
||||
fi, err := os.Stat("/dev/" + devName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stat, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if s.infoCache == nil {
|
||||
s.infoCache = map[string]diskInfoCache{}
|
||||
}
|
||||
ic, ok := s.infoCache[devName]
|
||||
if ok {
|
||||
return ic.values, nil
|
||||
} else {
|
||||
ic = diskInfoCache{
|
||||
stat: *stat,
|
||||
values: map[string]string{},
|
||||
}
|
||||
s.infoCache[devName] = ic
|
||||
}
|
||||
di := ic.values
|
||||
|
||||
major := stat.Rdev >> 8 & 0xff
|
||||
minor := stat.Rdev & 0xff
|
||||
|
||||
f, err := os.Open(fmt.Sprintf("%s/b%d:%d", udevPath, major, minor))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
scnr := bufio.NewScanner(f)
|
||||
|
||||
for scnr.Scan() {
|
||||
l := scnr.Text()
|
||||
if len(l) < 4 || l[:2] != "E:" {
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(l[2:], "=", 2)
|
||||
if len(kv) < 2 {
|
||||
continue
|
||||
}
|
||||
di[kv[0]] = kv[1]
|
||||
}
|
||||
|
||||
return di, nil
|
||||
}
|
||||
101
plugins/inputs/system/disk_linux_test.go
Normal file
101
plugins/inputs/system/disk_linux_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// +build linux
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var nullDiskInfo = []byte(`
|
||||
E:MY_PARAM_1=myval1
|
||||
E:MY_PARAM_2=myval2
|
||||
`)
|
||||
|
||||
// setupNullDisk sets up fake udev info as if /dev/null were a disk.
|
||||
func setupNullDisk(t *testing.T) func() error {
|
||||
td, err := ioutil.TempDir("", ".telegraf.TestDiskInfo")
|
||||
require.NoError(t, err)
|
||||
|
||||
origUdevPath := udevPath
|
||||
|
||||
cleanFunc := func() error {
|
||||
udevPath = origUdevPath
|
||||
return os.RemoveAll(td)
|
||||
}
|
||||
|
||||
udevPath = td
|
||||
err = ioutil.WriteFile(td+"/b1:3", nullDiskInfo, 0644) // 1:3 is the 'null' device
|
||||
if err != nil {
|
||||
cleanFunc()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return cleanFunc
|
||||
}
|
||||
|
||||
func TestDiskInfo(t *testing.T) {
|
||||
clean := setupNullDisk(t)
|
||||
defer clean()
|
||||
|
||||
s := &DiskIOStats{}
|
||||
di, err := s.diskInfo("null")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "myval1", di["MY_PARAM_1"])
|
||||
assert.Equal(t, "myval2", di["MY_PARAM_2"])
|
||||
|
||||
// test that data is cached
|
||||
err = clean()
|
||||
require.NoError(t, err)
|
||||
|
||||
di, err = s.diskInfo("null")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "myval1", di["MY_PARAM_1"])
|
||||
assert.Equal(t, "myval2", di["MY_PARAM_2"])
|
||||
|
||||
// unfortunately we can't adjust mtime on /dev/null to test cache invalidation
|
||||
}
|
||||
|
||||
// DiskIOStats.diskName isn't a linux specific function, but dependent
|
||||
// functions are a no-op on non-Linux.
|
||||
func TestDiskIOStats_diskName(t *testing.T) {
|
||||
defer setupNullDisk(t)()
|
||||
|
||||
tests := []struct {
|
||||
templates []string
|
||||
expected string
|
||||
}{
|
||||
{[]string{"$MY_PARAM_1"}, "myval1"},
|
||||
{[]string{"${MY_PARAM_1}"}, "myval1"},
|
||||
{[]string{"x$MY_PARAM_1"}, "xmyval1"},
|
||||
{[]string{"x${MY_PARAM_1}x"}, "xmyval1x"},
|
||||
{[]string{"$MISSING", "$MY_PARAM_1"}, "myval1"},
|
||||
{[]string{"$MY_PARAM_1", "$MY_PARAM_2"}, "myval1"},
|
||||
{[]string{"$MISSING"}, "null"},
|
||||
{[]string{"$MY_PARAM_1/$MY_PARAM_2"}, "myval1/myval2"},
|
||||
{[]string{"$MY_PARAM_2/$MISSING"}, "null"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
s := DiskIOStats{
|
||||
NameTemplates: tc.templates,
|
||||
}
|
||||
assert.Equal(t, tc.expected, s.diskName("null"), "Templates: %#v", tc.templates)
|
||||
}
|
||||
}
|
||||
|
||||
// DiskIOStats.diskTags isn't a linux specific function, but dependent
|
||||
// functions are a no-op on non-Linux.
|
||||
func TestDiskIOStats_diskTags(t *testing.T) {
|
||||
defer setupNullDisk(t)()
|
||||
|
||||
s := &DiskIOStats{
|
||||
DeviceTags: []string{"MY_PARAM_2"},
|
||||
}
|
||||
dt := s.diskTags("null")
|
||||
assert.Equal(t, map[string]string{"MY_PARAM_2": "myval2"}, dt)
|
||||
}
|
||||
9
plugins/inputs/system/disk_other.go
Normal file
9
plugins/inputs/system/disk_other.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build !linux
|
||||
|
||||
package system
|
||||
|
||||
type diskInfoCache struct{}
|
||||
|
||||
func (s *DiskIOStats) diskInfo(devName string) (map[string]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -110,13 +110,23 @@ var sanitizedChars = strings.NewReplacer("/sec", "_persec", "/Sec", "_persec",
|
||||
" ", "_", "%", "Percent", `\`, "")
|
||||
|
||||
func (m *Win_PerfCounters) AddItem(metrics *itemList, query string, objectName string, counter string, instance string,
|
||||
measurement string, include_total bool) {
|
||||
measurement string, include_total bool) error {
|
||||
|
||||
var handle PDH_HQUERY
|
||||
var counterHandle PDH_HCOUNTER
|
||||
ret := PdhOpenQuery(0, 0, &handle)
|
||||
ret = PdhAddCounter(handle, query, 0, &counterHandle)
|
||||
_ = ret
|
||||
if m.PreVistaSupport {
|
||||
ret = PdhAddCounter(handle, query, 0, &counterHandle)
|
||||
} else {
|
||||
ret = PdhAddEnglishCounter(handle, query, 0, &counterHandle)
|
||||
}
|
||||
|
||||
// Call PdhCollectQueryData one time to check existance of the counter
|
||||
ret = PdhCollectQueryData(handle)
|
||||
if ret != ERROR_SUCCESS {
|
||||
ret = PdhCloseQuery(handle)
|
||||
return errors.New("Invalid query for Performance Counters")
|
||||
}
|
||||
|
||||
temp := &item{query, objectName, counter, instance, measurement,
|
||||
include_total, handle, counterHandle}
|
||||
@@ -127,39 +137,6 @@ func (m *Win_PerfCounters) AddItem(metrics *itemList, query string, objectName s
|
||||
metrics.items = make(map[int]*item)
|
||||
}
|
||||
metrics.items[index] = temp
|
||||
}
|
||||
|
||||
func (m *Win_PerfCounters) InvalidObject(exists uint32, query string, PerfObject perfobject, instance string, counter string) error {
|
||||
if exists == 3221228472 { // PDH_CSTATUS_NO_OBJECT
|
||||
if PerfObject.FailOnMissing {
|
||||
err := errors.New("Performance object does not exist")
|
||||
return err
|
||||
} else {
|
||||
fmt.Printf("Performance Object '%s' does not exist in query: %s\n", PerfObject.ObjectName, query)
|
||||
}
|
||||
} else if exists == 3221228473 { // PDH_CSTATUS_NO_COUNTER
|
||||
|
||||
if PerfObject.FailOnMissing {
|
||||
err := errors.New("Counter in Performance object does not exist")
|
||||
return err
|
||||
} else {
|
||||
fmt.Printf("Counter '%s' does not exist in query: %s\n", counter, query)
|
||||
}
|
||||
} else if exists == 2147485649 { // PDH_CSTATUS_NO_INSTANCE
|
||||
if PerfObject.FailOnMissing {
|
||||
err := errors.New("Instance in Performance object does not exist")
|
||||
return err
|
||||
} else {
|
||||
fmt.Printf("Instance '%s' does not exist in query: %s\n", instance, query)
|
||||
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Invalid result: %v, query: %s\n", exists, query)
|
||||
if PerfObject.FailOnMissing {
|
||||
err := errors.New("Invalid query for Performance Counters")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -188,17 +165,18 @@ func (m *Win_PerfCounters) ParseConfig(metrics *itemList) error {
|
||||
query = "\\" + objectname + "(" + instance + ")\\" + counter
|
||||
}
|
||||
|
||||
var exists uint32 = PdhValidatePath(query)
|
||||
err := m.AddItem(metrics, query, objectname, counter, instance,
|
||||
PerfObject.Measurement, PerfObject.IncludeTotal)
|
||||
|
||||
if exists == ERROR_SUCCESS {
|
||||
if err == nil {
|
||||
if m.PrintValid {
|
||||
fmt.Printf("Valid: %s\n", query)
|
||||
}
|
||||
m.AddItem(metrics, query, objectname, counter, instance,
|
||||
PerfObject.Measurement, PerfObject.IncludeTotal)
|
||||
} else {
|
||||
if PerfObject.FailOnMissing || PerfObject.WarnOnMissing {
|
||||
err := m.InvalidObject(exists, query, PerfObject, instance, counter)
|
||||
fmt.Printf("Invalid query: %s\n", query)
|
||||
}
|
||||
if PerfObject.FailOnMissing {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ import (
|
||||
_ "github.com/influxdata/telegraf/plugins/outputs/opentsdb"
|
||||
_ "github.com/influxdata/telegraf/plugins/outputs/prometheus_client"
|
||||
_ "github.com/influxdata/telegraf/plugins/outputs/riemann"
|
||||
_ "github.com/influxdata/telegraf/plugins/outputs/riemann_legacy"
|
||||
_ "github.com/influxdata/telegraf/plugins/outputs/socket_writer"
|
||||
)
|
||||
|
||||
@@ -7,3 +7,37 @@ If RoutingTag is empty, then empty routing key will be used.
|
||||
Metrics are grouped in batches by RoutingTag.
|
||||
|
||||
This plugin doesn't bind exchange to a queue, so it should be done by consumer.
|
||||
|
||||
### Configuration:
|
||||
|
||||
```
|
||||
# Configuration for the AMQP server to send metrics to
|
||||
[[outputs.amqp]]
|
||||
## AMQP url
|
||||
url = "amqp://localhost:5672/influxdb"
|
||||
## AMQP exchange
|
||||
exchange = "telegraf"
|
||||
## Auth method. PLAIN and EXTERNAL are supported
|
||||
# auth_method = "PLAIN"
|
||||
## Telegraf tag to use as a routing key
|
||||
## ie, if this tag exists, it's value will be used as the routing key
|
||||
routing_tag = "host"
|
||||
|
||||
## InfluxDB retention policy
|
||||
# retention_policy = "default"
|
||||
## InfluxDB database
|
||||
# database = "telegraf"
|
||||
|
||||
## Optional SSL Config
|
||||
# ssl_ca = "/etc/telegraf/ca.pem"
|
||||
# ssl_cert = "/etc/telegraf/cert.pem"
|
||||
# ssl_key = "/etc/telegraf/key.pem"
|
||||
## Use SSL but skip chain & host verification
|
||||
# insecure_skip_verify = false
|
||||
|
||||
## Data format to output.
|
||||
## Each data format has it's own unique set of configuration options, read
|
||||
## more about them here:
|
||||
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md
|
||||
data_format = "influx"
|
||||
```
|
||||
|
||||
@@ -2,6 +2,7 @@ package graphite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
@@ -71,6 +72,31 @@ func (g *Graphite) Description() string {
|
||||
return "Configuration for Graphite server to send metrics to"
|
||||
}
|
||||
|
||||
// We need check eof as we can write to nothing without noticing anything is wrong
|
||||
// the connection stays in a close_wait
|
||||
// We can detect that by finding an eof
|
||||
// if not for this, we can happily write and flush without getting errors (in Go) but getting RST tcp packets back (!)
|
||||
// props to Tv via the authors of carbon-relay-ng` for this trick.
|
||||
func checkEOF(conn net.Conn) {
|
||||
b := make([]byte, 1024)
|
||||
conn.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
|
||||
num, err := conn.Read(b)
|
||||
if err == io.EOF {
|
||||
log.Printf("E! Conn %s is closed. closing conn explicitly", conn)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
// just in case i misunderstand something or the remote behaves badly
|
||||
if num != 0 {
|
||||
log.Printf("I! conn %s .conn.Read data? did not expect that. data: %s\n", conn, b[:num])
|
||||
}
|
||||
// Log non-timeout errors or close.
|
||||
if e, ok := err.(net.Error); !(ok && e.Timeout()) {
|
||||
log.Printf("E! conn %s checkEOF .conn.Read returned err != EOF, which is unexpected. closing conn. error: %s\n", conn, err)
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Choose a random server in the cluster to write to until a successful write
|
||||
// occurs, logging each unsuccessful. If all servers fail, return error.
|
||||
func (g *Graphite) Write(metrics []telegraf.Metric) error {
|
||||
@@ -91,13 +117,13 @@ func (g *Graphite) Write(metrics []telegraf.Metric) error {
|
||||
|
||||
// This will get set to nil if a successful write occurs
|
||||
err = errors.New("Could not write to any Graphite server in cluster\n")
|
||||
|
||||
// Send data to a random server
|
||||
p := rand.Perm(len(g.conns))
|
||||
for _, n := range p {
|
||||
if g.Timeout > 0 {
|
||||
g.conns[n].SetWriteDeadline(time.Now().Add(time.Duration(g.Timeout) * time.Second))
|
||||
}
|
||||
checkEOF(g.conns[n])
|
||||
if _, e := g.conns[n].Write(batch); e != nil {
|
||||
// Error
|
||||
log.Println("E! Graphite Error: " + e.Error())
|
||||
@@ -110,6 +136,7 @@ func (g *Graphite) Write(metrics []telegraf.Metric) error {
|
||||
}
|
||||
// try to reconnect
|
||||
if err != nil {
|
||||
log.Println("E! Reconnecting: ")
|
||||
g.Connect()
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -43,7 +43,8 @@ func TestGraphiteOK(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
// Start TCP server
|
||||
wg.Add(1)
|
||||
go TCPServer(t, &wg)
|
||||
t.Log("Starting server")
|
||||
go TCPServer1(t, &wg)
|
||||
// Give the fake graphite TCP server some time to start:
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
@@ -51,6 +52,7 @@ func TestGraphiteOK(t *testing.T) {
|
||||
g := Graphite{
|
||||
Prefix: "my.prefix",
|
||||
}
|
||||
|
||||
// Init metrics
|
||||
m1, _ := metric.New(
|
||||
"mymeasurement",
|
||||
@@ -72,29 +74,58 @@ func TestGraphiteOK(t *testing.T) {
|
||||
)
|
||||
|
||||
// Prepare point list
|
||||
metrics := []telegraf.Metric{m1, m2, m3}
|
||||
metrics := []telegraf.Metric{m1}
|
||||
metrics2 := []telegraf.Metric{m2, m3}
|
||||
err1 := g.Connect()
|
||||
require.NoError(t, err1)
|
||||
// Send Data
|
||||
t.Log("Send first data")
|
||||
err2 := g.Write(metrics)
|
||||
require.NoError(t, err2)
|
||||
|
||||
// Waiting TCPserver
|
||||
wg.Wait()
|
||||
t.Log("Finished Waiting for first data")
|
||||
var wg2 sync.WaitGroup
|
||||
// Start TCP server
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
wg2.Add(1)
|
||||
go TCPServer2(t, &wg2)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
//Write but expect an error, but reconnect
|
||||
g.Write(metrics2)
|
||||
err3 := g.Write(metrics2)
|
||||
t.Log("Finished writing second data, it should have failed")
|
||||
//Actually write the new metrics
|
||||
|
||||
require.NoError(t, err3)
|
||||
t.Log("Finished writing third data")
|
||||
wg2.Wait()
|
||||
g.Close()
|
||||
}
|
||||
|
||||
func TCPServer(t *testing.T, wg *sync.WaitGroup) {
|
||||
tcpServer, _ := net.Listen("tcp", "127.0.0.1:2003")
|
||||
func TCPServer1(t *testing.T, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
conn, _ := tcpServer.Accept()
|
||||
tcpServer, _ := net.Listen("tcp", "127.0.0.1:2003")
|
||||
conn, _ := (tcpServer).Accept()
|
||||
reader := bufio.NewReader(conn)
|
||||
tp := textproto.NewReader(reader)
|
||||
data1, _ := tp.ReadLine()
|
||||
assert.Equal(t, "my.prefix.192_168_0_1.mymeasurement.myfield 3.14 1289430000", data1)
|
||||
conn.Close()
|
||||
tcpServer.Close()
|
||||
}
|
||||
|
||||
func TCPServer2(t *testing.T, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
tcpServer, _ := net.Listen("tcp", "127.0.0.1:2003")
|
||||
conn2, _ := (tcpServer).Accept()
|
||||
reader := bufio.NewReader(conn2)
|
||||
tp := textproto.NewReader(reader)
|
||||
data2, _ := tp.ReadLine()
|
||||
assert.Equal(t, "my.prefix.192_168_0_1.mymeasurement 3.14 1289430000", data2)
|
||||
data3, _ := tp.ReadLine()
|
||||
assert.Equal(t, "my.prefix.192_168_0_1.my_measurement 3.14 1289430000", data3)
|
||||
conn.Close()
|
||||
conn2.Close()
|
||||
tcpServer.Close()
|
||||
}
|
||||
|
||||
22
plugins/outputs/influxdb/client/client.go
Normal file
22
plugins/outputs/influxdb/client/client.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package client
|
||||
|
||||
import "io"
|
||||
|
||||
type Client interface {
|
||||
Query(command string) error
|
||||
|
||||
Write(b []byte) (int, error)
|
||||
WriteWithParams(b []byte, params WriteParams) (int, error)
|
||||
|
||||
WriteStream(b io.Reader, contentLength int) (int, error)
|
||||
WriteStreamWithParams(b io.Reader, contentLength int, params WriteParams) (int, error)
|
||||
|
||||
Close() error
|
||||
}
|
||||
|
||||
type WriteParams struct {
|
||||
Database string
|
||||
RetentionPolicy string
|
||||
Precision string
|
||||
Consistency string
|
||||
}
|
||||
269
plugins/outputs/influxdb/client/http.go
Normal file
269
plugins/outputs/influxdb/client/http.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultRequestTimeout = time.Second * 5
|
||||
)
|
||||
|
||||
//
|
||||
func NewHTTP(config HTTPConfig, defaultWP WriteParams) (Client, error) {
|
||||
// validate required parameters:
|
||||
if len(config.URL) == 0 {
|
||||
return nil, fmt.Errorf("config.URL is required to create an HTTP client")
|
||||
}
|
||||
if len(defaultWP.Database) == 0 {
|
||||
return nil, fmt.Errorf("A default database is required to create an HTTP client")
|
||||
}
|
||||
|
||||
// set defaults:
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = defaultRequestTimeout
|
||||
}
|
||||
|
||||
// parse URL:
|
||||
u, err := url.Parse(config.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing config.URL: %s", err)
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return nil, fmt.Errorf("config.URL scheme must be http(s), got %s", u.Scheme)
|
||||
}
|
||||
|
||||
return &httpClient{
|
||||
writeURL: writeURL(u, defaultWP),
|
||||
config: config,
|
||||
url: u,
|
||||
client: &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: config.TLSConfig,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
// URL should be of the form "http://host:port" (REQUIRED)
|
||||
URL string
|
||||
|
||||
// UserAgent sets the User-Agent header.
|
||||
UserAgent string
|
||||
|
||||
// Timeout specifies a time limit for requests made by this
|
||||
// Client. The timeout includes connection time, any
|
||||
// redirects, and reading the response body. The timer remains
|
||||
// running after Get, Head, Post, or Do return and will
|
||||
// interrupt reading of the Response.Body.
|
||||
//
|
||||
// A Timeout of zero means no timeout.
|
||||
Timeout time.Duration
|
||||
|
||||
// Username is the basic auth username for the server.
|
||||
Username string
|
||||
// Password is the basic auth password for the server.
|
||||
Password string
|
||||
|
||||
// TLSConfig is the tls auth settings to use for each request.
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// Gzip, if true, compresses each payload using gzip.
|
||||
// TODO
|
||||
// Gzip bool
|
||||
}
|
||||
|
||||
// Response represents a list of statement results.
|
||||
type Response struct {
|
||||
// ignore Results:
|
||||
Results []interface{} `json:"-"`
|
||||
Err string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Error returns the first error from any statement.
|
||||
// Returns nil if no errors occurred on any statements.
|
||||
func (r *Response) Error() error {
|
||||
if r.Err != "" {
|
||||
return fmt.Errorf(r.Err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type httpClient struct {
|
||||
writeURL string
|
||||
config HTTPConfig
|
||||
client *http.Client
|
||||
url *url.URL
|
||||
}
|
||||
|
||||
func (c *httpClient) Query(command string) error {
|
||||
req, err := c.makeRequest(queryURL(c.url, command), bytes.NewReader([]byte("")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.doRequest(req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (c *httpClient) Write(b []byte) (int, error) {
|
||||
req, err := c.makeWriteRequest(bytes.NewReader(b), len(b), c.writeURL)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
err = c.doRequest(req, http.StatusNoContent)
|
||||
if err == nil {
|
||||
return len(b), nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func (c *httpClient) WriteWithParams(b []byte, wp WriteParams) (int, error) {
|
||||
req, err := c.makeWriteRequest(bytes.NewReader(b), len(b), writeURL(c.url, wp))
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
err = c.doRequest(req, http.StatusNoContent)
|
||||
if err == nil {
|
||||
return len(b), nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func (c *httpClient) WriteStream(r io.Reader, contentLength int) (int, error) {
|
||||
req, err := c.makeWriteRequest(r, contentLength, c.writeURL)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
err = c.doRequest(req, http.StatusNoContent)
|
||||
if err == nil {
|
||||
return contentLength, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func (c *httpClient) WriteStreamWithParams(
|
||||
r io.Reader,
|
||||
contentLength int,
|
||||
wp WriteParams,
|
||||
) (int, error) {
|
||||
req, err := c.makeWriteRequest(r, contentLength, writeURL(c.url, wp))
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
err = c.doRequest(req, http.StatusNoContent)
|
||||
if err == nil {
|
||||
return contentLength, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func (c *httpClient) doRequest(
|
||||
req *http.Request,
|
||||
expectedCode int,
|
||||
) error {
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := resp.StatusCode
|
||||
// If it's a "no content" response, then release and return nil
|
||||
if code == http.StatusNoContent {
|
||||
return nil
|
||||
}
|
||||
|
||||
// not a "no content" response, so parse the result:
|
||||
var response Response
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Fatal error reading body: %s", err)
|
||||
}
|
||||
decErr := json.Unmarshal(body, &response)
|
||||
|
||||
// If we got a JSON decode error, send that back
|
||||
if decErr != nil {
|
||||
err = fmt.Errorf("Unable to decode json: received status code %d err: %s", code, decErr)
|
||||
}
|
||||
// Unexpected response code OR error in JSON response body overrides
|
||||
// a JSON decode error:
|
||||
if code != expectedCode || response.Error() != nil {
|
||||
err = fmt.Errorf("Response Error: Status Code [%d], expected [%d], [%v]",
|
||||
code, expectedCode, response.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *httpClient) makeWriteRequest(
|
||||
body io.Reader,
|
||||
contentLength int,
|
||||
writeURL string,
|
||||
) (*http.Request, error) {
|
||||
req, err := c.makeRequest(writeURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Length", fmt.Sprint(contentLength))
|
||||
// TODO
|
||||
// if gzip {
|
||||
// req.Header.Set("Content-Encoding", "gzip")
|
||||
// }
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *httpClient) makeRequest(uri string, body io.Reader) (*http.Request, error) {
|
||||
req, err := http.NewRequest("POST", uri, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "text/plain")
|
||||
req.Header.Set("User-Agent", c.config.UserAgent)
|
||||
if c.config.Username != "" && c.config.Password != "" {
|
||||
req.SetBasicAuth(c.config.Username, c.config.Password)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *httpClient) Close() error {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeURL(u *url.URL, wp WriteParams) string {
|
||||
params := url.Values{}
|
||||
params.Set("db", wp.Database)
|
||||
if wp.RetentionPolicy != "" {
|
||||
params.Set("rp", wp.RetentionPolicy)
|
||||
}
|
||||
if wp.Precision != "n" && wp.Precision != "" {
|
||||
params.Set("precision", wp.Precision)
|
||||
}
|
||||
if wp.Consistency != "one" && wp.Consistency != "" {
|
||||
params.Set("consistency", wp.Consistency)
|
||||
}
|
||||
|
||||
u.RawQuery = params.Encode()
|
||||
u.Path = "write"
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func queryURL(u *url.URL, command string) string {
|
||||
params := url.Values{}
|
||||
params.Set("q", command)
|
||||
|
||||
u.RawQuery = params.Encode()
|
||||
u.Path = "query"
|
||||
return u.String()
|
||||
}
|
||||
343
plugins/outputs/influxdb/client/http_test.go
Normal file
343
plugins/outputs/influxdb/client/http_test.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHTTPClient_Write(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/write":
|
||||
// test form values:
|
||||
if r.FormValue("db") != "test" {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"wrong db name"}`)
|
||||
}
|
||||
if r.FormValue("rp") != "policy" {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"wrong rp name"}`)
|
||||
}
|
||||
if r.FormValue("precision") != "ns" {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"wrong precision"}`)
|
||||
}
|
||||
if r.FormValue("consistency") != "all" {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"wrong consistency"}`)
|
||||
}
|
||||
// test that user agent is set properly
|
||||
if r.UserAgent() != "test-agent" {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"wrong agent name"}`)
|
||||
}
|
||||
// test basic auth params
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"basic auth not set"}`)
|
||||
}
|
||||
if user != "test-user" || pass != "test-password" {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"basic auth incorrect"}`)
|
||||
}
|
||||
|
||||
// Validate Content-Length Header
|
||||
if r.ContentLength != 13 {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
msg := fmt.Sprintf(`{"results":[{}],"error":"Content-Length: expected [13], got [%d]"}`, r.ContentLength)
|
||||
fmt.Fprintln(w, msg)
|
||||
}
|
||||
|
||||
// Validate the request body:
|
||||
buf := make([]byte, 100)
|
||||
n, _ := r.Body.Read(buf)
|
||||
expected := "cpu value=99"
|
||||
got := string(buf[0 : n-1])
|
||||
if expected != got {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
msg := fmt.Sprintf(`{"results":[{}],"error":"expected [%s], got [%s]"}`, expected, got)
|
||||
fmt.Fprintln(w, msg)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
case "/query":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}]}`)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
config := HTTPConfig{
|
||||
URL: ts.URL,
|
||||
UserAgent: "test-agent",
|
||||
Username: "test-user",
|
||||
Password: "test-password",
|
||||
}
|
||||
wp := WriteParams{
|
||||
Database: "test",
|
||||
RetentionPolicy: "policy",
|
||||
Precision: "ns",
|
||||
Consistency: "all",
|
||||
}
|
||||
client, err := NewHTTP(config, wp)
|
||||
defer client.Close()
|
||||
assert.NoError(t, err)
|
||||
n, err := client.Write([]byte("cpu value=99\n"))
|
||||
assert.Equal(t, 13, n)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = client.WriteStream(bytes.NewReader([]byte("cpu value=99\n")), 13)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPClient_WriteParamsOverride(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/write":
|
||||
// test that database is set properly
|
||||
if r.FormValue("db") != "override" {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"wrong db name"}`)
|
||||
}
|
||||
|
||||
// Validate the request body:
|
||||
buf := make([]byte, 100)
|
||||
n, _ := r.Body.Read(buf)
|
||||
expected := "cpu value=99"
|
||||
got := string(buf[0 : n-1])
|
||||
if expected != got {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
msg := fmt.Sprintf(`{"results":[{}],"error":"expected [%s], got [%s]"}`, expected, got)
|
||||
fmt.Fprintln(w, msg)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
case "/query":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}]}`)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
config := HTTPConfig{
|
||||
URL: ts.URL,
|
||||
}
|
||||
defaultWP := WriteParams{
|
||||
Database: "test",
|
||||
}
|
||||
client, err := NewHTTP(config, defaultWP)
|
||||
defer client.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// test that WriteWithParams overrides the default write params
|
||||
wp := WriteParams{
|
||||
Database: "override",
|
||||
}
|
||||
n, err := client.WriteWithParams([]byte("cpu value=99\n"), wp)
|
||||
assert.Equal(t, 13, n)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = client.WriteStreamWithParams(bytes.NewReader([]byte("cpu value=99\n")), 13, wp)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPClient_Write_Errors(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/write":
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
case "/query":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}]}`)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
config := HTTPConfig{
|
||||
URL: ts.URL,
|
||||
}
|
||||
defaultWP := WriteParams{
|
||||
Database: "test",
|
||||
}
|
||||
client, err := NewHTTP(config, defaultWP)
|
||||
defer client.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
lp := []byte("cpu value=99\n")
|
||||
n, err := client.Write(lp)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Error(t, err)
|
||||
|
||||
n, err = client.WriteStream(bytes.NewReader(lp), 13)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Error(t, err)
|
||||
|
||||
wp := WriteParams{
|
||||
Database: "override",
|
||||
}
|
||||
n, err = client.WriteWithParams(lp, wp)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Error(t, err)
|
||||
|
||||
n, err = client.WriteStreamWithParams(bytes.NewReader(lp), 13, wp)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewHTTPErrors(t *testing.T) {
|
||||
// No URL:
|
||||
config := HTTPConfig{}
|
||||
defaultWP := WriteParams{
|
||||
Database: "test",
|
||||
}
|
||||
client, err := NewHTTP(config, defaultWP)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
|
||||
// No Database:
|
||||
config = HTTPConfig{
|
||||
URL: "http://localhost:8086",
|
||||
}
|
||||
defaultWP = WriteParams{}
|
||||
client, err = NewHTTP(config, defaultWP)
|
||||
assert.Nil(t, client)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Invalid URL:
|
||||
config = HTTPConfig{
|
||||
URL: "http://192.168.0.%31:8080/",
|
||||
}
|
||||
defaultWP = WriteParams{
|
||||
Database: "test",
|
||||
}
|
||||
client, err = NewHTTP(config, defaultWP)
|
||||
assert.Nil(t, client)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Invalid URL scheme:
|
||||
config = HTTPConfig{
|
||||
URL: "mailto://localhost:8086",
|
||||
}
|
||||
defaultWP = WriteParams{
|
||||
Database: "test",
|
||||
}
|
||||
client, err = NewHTTP(config, defaultWP)
|
||||
assert.Nil(t, client)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPClient_Query(t *testing.T) {
|
||||
command := "CREATE DATABASE test"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/write":
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case "/query":
|
||||
// validate the create database command is correct
|
||||
got := r.FormValue("q")
|
||||
if got != command {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
msg := fmt.Sprintf(`{"results":[{}],"error":"got %s, expected %s"}`, got, command)
|
||||
fmt.Fprintln(w, msg)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}]}`)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
config := HTTPConfig{
|
||||
URL: ts.URL,
|
||||
}
|
||||
defaultWP := WriteParams{
|
||||
Database: "test",
|
||||
}
|
||||
client, err := NewHTTP(config, defaultWP)
|
||||
defer client.Close()
|
||||
assert.NoError(t, err)
|
||||
err = client.Query(command)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPClient_Query_ResponseError(t *testing.T) {
|
||||
command := "CREATE DATABASE test"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/write":
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case "/query":
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
msg := fmt.Sprintf(`{"results":[{}],"error":"couldnt create database"}`)
|
||||
fmt.Fprintln(w, msg)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
config := HTTPConfig{
|
||||
URL: ts.URL,
|
||||
}
|
||||
defaultWP := WriteParams{
|
||||
Database: "test",
|
||||
}
|
||||
client, err := NewHTTP(config, defaultWP)
|
||||
defer client.Close()
|
||||
assert.NoError(t, err)
|
||||
err = client.Query(command)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPClient_Query_JSONDecodeError(t *testing.T) {
|
||||
command := "CREATE DATABASE test"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/write":
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case "/query":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// write JSON missing a ']'
|
||||
msg := fmt.Sprintf(`{"results":[{}}`)
|
||||
fmt.Fprintln(w, msg)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
config := HTTPConfig{
|
||||
URL: ts.URL,
|
||||
}
|
||||
defaultWP := WriteParams{
|
||||
Database: "test",
|
||||
}
|
||||
client, err := NewHTTP(config, defaultWP)
|
||||
defer client.Close()
|
||||
assert.NoError(t, err)
|
||||
err = client.Query(command)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "json")
|
||||
}
|
||||
99
plugins/outputs/influxdb/client/udp.go
Normal file
99
plugins/outputs/influxdb/client/udp.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const (
|
||||
// UDPPayloadSize is a reasonable default payload size for UDP packets that
|
||||
// could be travelling over the internet.
|
||||
UDPPayloadSize = 512
|
||||
)
|
||||
|
||||
// UDPConfig is the config data needed to create a UDP Client
|
||||
type UDPConfig struct {
|
||||
// URL should be of the form "udp://host:port"
|
||||
// or "udp://[ipv6-host%zone]:port".
|
||||
URL string
|
||||
|
||||
// PayloadSize is the maximum size of a UDP client message, optional
|
||||
// Tune this based on your network. Defaults to UDPPayloadSize.
|
||||
PayloadSize int
|
||||
}
|
||||
|
||||
func NewUDP(config UDPConfig) (Client, error) {
|
||||
p, err := url.Parse(config.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing UDP url [%s]: %s", config.URL, err)
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", p.Host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error resolving UDP Address [%s]: %s", p.Host, err)
|
||||
}
|
||||
|
||||
conn, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error dialing UDP address [%s]: %s",
|
||||
udpAddr.String(), err)
|
||||
}
|
||||
|
||||
size := config.PayloadSize
|
||||
if size == 0 {
|
||||
size = UDPPayloadSize
|
||||
}
|
||||
buf := make([]byte, size)
|
||||
return &udpClient{conn: conn, buffer: buf}, nil
|
||||
}
|
||||
|
||||
type udpClient struct {
|
||||
conn *net.UDPConn
|
||||
buffer []byte
|
||||
}
|
||||
|
||||
func (c *udpClient) Query(command string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *udpClient) Write(b []byte) (int, error) {
|
||||
return c.WriteStream(bytes.NewReader(b), -1)
|
||||
}
|
||||
|
||||
// write params are ignored by the UDP client
|
||||
func (c *udpClient) WriteWithParams(b []byte, wp WriteParams) (int, error) {
|
||||
return c.WriteStream(bytes.NewReader(b), -1)
|
||||
}
|
||||
|
||||
// contentLength is ignored by the UDP client.
|
||||
func (c *udpClient) WriteStream(r io.Reader, contentLength int) (int, error) {
|
||||
var totaln int
|
||||
for {
|
||||
nR, err := r.Read(c.buffer)
|
||||
if nR == 0 {
|
||||
break
|
||||
}
|
||||
if err != io.EOF && err != nil {
|
||||
return totaln, err
|
||||
}
|
||||
nW, err := c.conn.Write(c.buffer[0:nR])
|
||||
totaln += nW
|
||||
if err != nil {
|
||||
return totaln, err
|
||||
}
|
||||
}
|
||||
return totaln, nil
|
||||
}
|
||||
|
||||
// contentLength is ignored by the UDP client.
|
||||
// write params are ignored by the UDP client
|
||||
func (c *udpClient) WriteStreamWithParams(r io.Reader, contentLength int, wp WriteParams) (int, error) {
|
||||
return c.WriteStream(r, -1)
|
||||
}
|
||||
|
||||
func (c *udpClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
163
plugins/outputs/influxdb/client/udp_test.go
Normal file
163
plugins/outputs/influxdb/client/udp_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUDPClient(t *testing.T) {
|
||||
config := UDPConfig{
|
||||
URL: "udp://localhost:8089",
|
||||
}
|
||||
client, err := NewUDP(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = client.Query("ANY QUERY RETURNS NIL")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, client.Close())
|
||||
}
|
||||
|
||||
func TestNewUDPClient_Errors(t *testing.T) {
|
||||
// url.Parse Error
|
||||
config := UDPConfig{
|
||||
URL: "udp://localhost%35:8089",
|
||||
}
|
||||
_, err := NewUDP(config)
|
||||
assert.Error(t, err)
|
||||
|
||||
// ResolveUDPAddr Error
|
||||
config = UDPConfig{
|
||||
URL: "udp://localhost:999999",
|
||||
}
|
||||
_, err = NewUDP(config)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUDPClient_Write(t *testing.T) {
|
||||
config := UDPConfig{
|
||||
URL: "udp://localhost:8199",
|
||||
}
|
||||
client, err := NewUDP(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
packets := make(chan string, 100)
|
||||
address, err := net.ResolveUDPAddr("udp", "localhost:8199")
|
||||
assert.NoError(t, err)
|
||||
listener, err := net.ListenUDP("udp", address)
|
||||
defer listener.Close()
|
||||
assert.NoError(t, err)
|
||||
go func() {
|
||||
buf := make([]byte, 200)
|
||||
for {
|
||||
n, _, err := listener.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
packets <- err.Error()
|
||||
}
|
||||
packets <- string(buf[0:n])
|
||||
}
|
||||
}()
|
||||
|
||||
// test sending simple metric
|
||||
time.Sleep(time.Second)
|
||||
n, err := client.Write([]byte("cpu value=99\n"))
|
||||
assert.Equal(t, n, 13)
|
||||
assert.NoError(t, err)
|
||||
pkt := <-packets
|
||||
assert.Equal(t, "cpu value=99\n", pkt)
|
||||
|
||||
metrics := `cpu value=99
|
||||
cpu value=55
|
||||
cpu value=44
|
||||
cpu value=101
|
||||
cpu value=91
|
||||
cpu value=92
|
||||
`
|
||||
// test sending packet with 6 metrics in a stream.
|
||||
reader := bytes.NewReader([]byte(metrics))
|
||||
// contentLength is ignored:
|
||||
n, err = client.WriteStream(reader, 10)
|
||||
assert.Equal(t, n, len(metrics))
|
||||
assert.NoError(t, err)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "cpu value=99\ncpu value=55\ncpu value=44\ncpu value=101\ncpu value=91\ncpu value=92\n", pkt)
|
||||
|
||||
//
|
||||
// Test that UDP packets get broken up properly:
|
||||
config2 := UDPConfig{
|
||||
URL: "udp://localhost:8199",
|
||||
PayloadSize: 25,
|
||||
}
|
||||
client2, err := NewUDP(config2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
wp := WriteParams{}
|
||||
|
||||
//
|
||||
// Using Write():
|
||||
buf := []byte(metrics)
|
||||
n, err = client2.WriteWithParams(buf, wp)
|
||||
assert.Equal(t, n, len(metrics))
|
||||
assert.NoError(t, err)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "cpu value=99\ncpu value=55", pkt)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "\ncpu value=44\ncpu value=1", pkt)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "01\ncpu value=91\ncpu value", pkt)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "=92\n", pkt)
|
||||
|
||||
//
|
||||
// Using WriteStream():
|
||||
reader = bytes.NewReader([]byte(metrics))
|
||||
n, err = client2.WriteStreamWithParams(reader, 10, wp)
|
||||
assert.Equal(t, n, len(metrics))
|
||||
assert.NoError(t, err)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "cpu value=99\ncpu value=55", pkt)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "\ncpu value=44\ncpu value=1", pkt)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "01\ncpu value=91\ncpu value", pkt)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "=92\n", pkt)
|
||||
|
||||
//
|
||||
// Using WriteStream() & a metric.Reader:
|
||||
config3 := UDPConfig{
|
||||
URL: "udp://localhost:8199",
|
||||
PayloadSize: 40,
|
||||
}
|
||||
client3, err := NewUDP(config3)
|
||||
assert.NoError(t, err)
|
||||
|
||||
now := time.Unix(1484142942, 0)
|
||||
m1, _ := metric.New("test", map[string]string{},
|
||||
map[string]interface{}{"value": 1.1}, now)
|
||||
m2, _ := metric.New("test", map[string]string{},
|
||||
map[string]interface{}{"value": 1.1}, now)
|
||||
m3, _ := metric.New("test", map[string]string{},
|
||||
map[string]interface{}{"value": 1.1}, now)
|
||||
ms := []telegraf.Metric{m1, m2, m3}
|
||||
mReader := metric.NewReader(ms)
|
||||
n, err = client3.WriteStreamWithParams(mReader, 10, wp)
|
||||
// 3 metrics at 35 bytes each (including the newline)
|
||||
assert.Equal(t, 105, n)
|
||||
assert.NoError(t, err)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "test value=1.1 1484142942000000000\n", pkt)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "test value=1.1 1484142942000000000\n", pkt)
|
||||
pkt = <-packets
|
||||
assert.Equal(t, "test value=1.1 1484142942000000000\n", pkt)
|
||||
|
||||
assert.NoError(t, client.Close())
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
package influxdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
|
||||
"github.com/influxdata/influxdb/client/v2"
|
||||
"github.com/influxdata/telegraf/plugins/outputs/influxdb/client"
|
||||
)
|
||||
|
||||
type InfluxDB struct {
|
||||
@@ -41,7 +40,7 @@ type InfluxDB struct {
|
||||
// Precision is only here for legacy support. It will be ignored.
|
||||
Precision string
|
||||
|
||||
conns []client.Client
|
||||
clients []client.Client
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
@@ -88,79 +87,56 @@ func (i *InfluxDB) Connect() error {
|
||||
urls = append(urls, i.URL)
|
||||
}
|
||||
|
||||
tlsCfg, err := internal.GetTLSConfig(
|
||||
tlsConfig, err := internal.GetTLSConfig(
|
||||
i.SSLCert, i.SSLKey, i.SSLCA, i.InsecureSkipVerify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var conns []client.Client
|
||||
for _, u := range urls {
|
||||
switch {
|
||||
case strings.HasPrefix(u, "udp"):
|
||||
parsed_url, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i.UDPPayload == 0 {
|
||||
i.UDPPayload = client.UDPPayloadSize
|
||||
}
|
||||
c, err := client.NewUDPClient(client.UDPConfig{
|
||||
Addr: parsed_url.Host,
|
||||
config := client.UDPConfig{
|
||||
URL: u,
|
||||
PayloadSize: i.UDPPayload,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conns = append(conns, c)
|
||||
c, err := client.NewUDP(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating UDP Client [%s]: %s", u, err)
|
||||
}
|
||||
i.clients = append(i.clients, c)
|
||||
default:
|
||||
// If URL doesn't start with "udp", assume HTTP client
|
||||
c, err := client.NewHTTPClient(client.HTTPConfig{
|
||||
Addr: u,
|
||||
Username: i.Username,
|
||||
Password: i.Password,
|
||||
UserAgent: i.UserAgent,
|
||||
config := client.HTTPConfig{
|
||||
URL: u,
|
||||
Timeout: i.Timeout.Duration,
|
||||
TLSConfig: tlsCfg,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
TLSConfig: tlsConfig,
|
||||
UserAgent: i.UserAgent,
|
||||
}
|
||||
wp := client.WriteParams{
|
||||
Database: i.Database,
|
||||
RetentionPolicy: i.RetentionPolicy,
|
||||
Consistency: i.WriteConsistency,
|
||||
}
|
||||
c, err := client.NewHTTP(config, wp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating HTTP Client [%s]: %s", u, err)
|
||||
}
|
||||
i.clients = append(i.clients, c)
|
||||
|
||||
err = createDatabase(c, i.Database)
|
||||
err = c.Query("CREATE DATABASE " + i.Database)
|
||||
if err != nil {
|
||||
log.Println("E! Database creation failed: " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
conns = append(conns, c)
|
||||
}
|
||||
}
|
||||
|
||||
i.conns = conns
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDatabase(c client.Client, database string) error {
|
||||
// Create Database if it doesn't exist
|
||||
_, err := c.Query(client.Query{
|
||||
Command: fmt.Sprintf("CREATE DATABASE \"%s\"", database),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *InfluxDB) Close() error {
|
||||
var errS string
|
||||
for j, _ := range i.conns {
|
||||
if err := i.conns[j].Close(); err != nil {
|
||||
errS += err.Error()
|
||||
}
|
||||
}
|
||||
if errS != "" {
|
||||
return fmt.Errorf("output influxdb close failed: %s", errS)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -175,34 +151,21 @@ func (i *InfluxDB) Description() string {
|
||||
// Choose a random server in the cluster to write to until a successful write
|
||||
// occurs, logging each unsuccessful. If all servers fail, return error.
|
||||
func (i *InfluxDB) Write(metrics []telegraf.Metric) error {
|
||||
if len(i.conns) == 0 {
|
||||
err := i.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
bp, err := client.NewBatchPoints(client.BatchPointsConfig{
|
||||
Database: i.Database,
|
||||
RetentionPolicy: i.RetentionPolicy,
|
||||
WriteConsistency: i.WriteConsistency,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, metric := range metrics {
|
||||
bp.AddPoint(metric.Point())
|
||||
bufsize := 0
|
||||
for _, m := range metrics {
|
||||
bufsize += m.Len()
|
||||
}
|
||||
r := metric.NewReader(metrics)
|
||||
|
||||
// This will get set to nil if a successful write occurs
|
||||
err = errors.New("Could not write to any InfluxDB server in cluster")
|
||||
err := fmt.Errorf("Could not write to any InfluxDB server in cluster")
|
||||
|
||||
p := rand.Perm(len(i.conns))
|
||||
p := rand.Perm(len(i.clients))
|
||||
for _, n := range p {
|
||||
if e := i.conns[n].Write(bp); e != nil {
|
||||
// If the database was not found, try to recreate it
|
||||
if _, e := i.clients[n].WriteStream(r, bufsize); e != nil {
|
||||
// If the database was not found, try to recreate it:
|
||||
if strings.Contains(e.Error(), "database not found") {
|
||||
if errc := createDatabase(i.conns[n], i.Database); errc != nil {
|
||||
if errc := i.clients[n].Query("CREATE DATABASE " + i.Database); errc != nil {
|
||||
log.Printf("E! Error: Database %s not found and failed to recreate\n",
|
||||
i.Database)
|
||||
}
|
||||
@@ -225,10 +188,12 @@ func (i *InfluxDB) Write(metrics []telegraf.Metric) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("influxdb", func() telegraf.Output {
|
||||
return &InfluxDB{
|
||||
Timeout: internal.Duration{Duration: time.Second * 5},
|
||||
}
|
||||
})
|
||||
func newInflux() *InfluxDB {
|
||||
return &InfluxDB{
|
||||
Timeout: internal.Duration{Duration: time.Second * 5},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("influxdb", func() telegraf.Output { return newInflux() })
|
||||
}
|
||||
|
||||
@@ -20,22 +20,147 @@ func TestUDPInflux(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
err = i.Write(testutil.MockMetrics())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, i.Close())
|
||||
}
|
||||
|
||||
func TestHTTPInflux(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}]}`)
|
||||
switch r.URL.Path {
|
||||
case "/write":
|
||||
// test that database is set properly
|
||||
if r.FormValue("db") != "test" {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
// test that user agent is set properly
|
||||
if r.UserAgent() != "telegraf" {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
case "/query":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}]}`)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
i := newInflux()
|
||||
i.URLs = []string{ts.URL}
|
||||
i.Database = "test"
|
||||
i.UserAgent = "telegraf"
|
||||
|
||||
err := i.Connect()
|
||||
require.NoError(t, err)
|
||||
err = i.Write(testutil.MockMetrics())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, i.Close())
|
||||
}
|
||||
|
||||
func TestUDPConnectError(t *testing.T) {
|
||||
i := InfluxDB{
|
||||
URLs: []string{"udp://foobar:8089"},
|
||||
}
|
||||
|
||||
err := i.Connect()
|
||||
require.Error(t, err)
|
||||
|
||||
i = InfluxDB{
|
||||
URLs: []string{"udp://localhost:9999999"},
|
||||
}
|
||||
|
||||
err = i.Connect()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPConnectError_InvalidURL(t *testing.T) {
|
||||
i := InfluxDB{
|
||||
URLs: []string{"http://foobar:8089"},
|
||||
}
|
||||
|
||||
err := i.Connect()
|
||||
require.Error(t, err)
|
||||
|
||||
i = InfluxDB{
|
||||
URLs: []string{"http://localhost:9999999"},
|
||||
}
|
||||
|
||||
err = i.Connect()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPConnectError_DatabaseCreateFail(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/query":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"test error"}`)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
i := InfluxDB{
|
||||
URLs: []string{ts.URL},
|
||||
URLs: []string{ts.URL},
|
||||
Database: "test",
|
||||
}
|
||||
|
||||
// database creation errors do not return an error from Connect
|
||||
// they are only logged.
|
||||
err := i.Connect()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, i.Close())
|
||||
}
|
||||
|
||||
func TestHTTPError_DatabaseNotFound(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/write":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"database not found"}`)
|
||||
case "/query":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"database not found"}`)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
i := InfluxDB{
|
||||
URLs: []string{ts.URL},
|
||||
Database: "test",
|
||||
}
|
||||
|
||||
err := i.Connect()
|
||||
require.NoError(t, err)
|
||||
err = i.Write(testutil.MockMetrics())
|
||||
require.Error(t, err)
|
||||
require.NoError(t, i.Close())
|
||||
}
|
||||
|
||||
// field type conflict does not return an error, instead we
|
||||
func TestHTTPError_FieldTypeConflict(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/write":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"results":[{}],"error":"field type conflict: input field \"value\" on measurement \"test\" is type integer, already exists as type float dropped=1"}`)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
i := InfluxDB{
|
||||
URLs: []string{ts.URL},
|
||||
Database: "test",
|
||||
}
|
||||
|
||||
err := i.Connect()
|
||||
require.NoError(t, err)
|
||||
err = i.Write(testutil.MockMetrics())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, i.Close())
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ func (k *Kafka) Connect() error {
|
||||
config.Producer.RequiredAcks = sarama.RequiredAcks(k.RequiredAcks)
|
||||
config.Producer.Compression = sarama.CompressionCodec(k.CompressionCodec)
|
||||
config.Producer.Retry.Max = k.MaxRetry
|
||||
config.Producer.Return.Successes = true
|
||||
|
||||
// Legacy support ssl config
|
||||
if k.Certificate != "" {
|
||||
|
||||
83
plugins/outputs/riemann/README.md
Normal file
83
plugins/outputs/riemann/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Riemann Output Plugin
|
||||
|
||||
This plugin writes to [Riemann](http://riemann.io/) via TCP or UDP.
|
||||
|
||||
### Configuration:
|
||||
|
||||
```toml
|
||||
# Configuration for Riemann to send metrics to
|
||||
[[outputs.riemann]]
|
||||
## The full TCP or UDP URL of the Riemann server
|
||||
url = "tcp://localhost:5555"
|
||||
|
||||
## Riemann event TTL, floating-point time in seconds.
|
||||
## Defines how long that an event is considered valid for in Riemann
|
||||
# ttl = 30.0
|
||||
|
||||
## Separator to use between measurement and field name in Riemann service name
|
||||
## This does not have any effect if 'measurement_as_attribute' is set to 'true'
|
||||
separator = "/"
|
||||
|
||||
## Set measurement name as Riemann attribute 'measurement', instead of prepending it to the Riemann service name
|
||||
# measurement_as_attribute = false
|
||||
|
||||
## Send string metrics as Riemann event states.
|
||||
## Unless enabled all string metrics will be ignored
|
||||
# string_as_state = false
|
||||
|
||||
## A list of tag keys whose values get sent as Riemann tags.
|
||||
## If empty, all Telegraf tag values will be sent as tags
|
||||
# tag_keys = ["telegraf","custom_tag"]
|
||||
|
||||
## Additional Riemann tags to send.
|
||||
# tags = ["telegraf-output"]
|
||||
|
||||
## Description for Riemann event
|
||||
# description_text = "metrics collected from telegraf"
|
||||
```
|
||||
|
||||
### Required parameters:
|
||||
|
||||
* `url`: The full TCP or UDP URL of the Riemann server to send events to.
|
||||
|
||||
### Optional parameters:
|
||||
|
||||
* `ttl`: Riemann event TTL, floating-point time in seconds. Defines how long that an event is considered valid for in Riemann.
|
||||
* `separator`: Separator to use between measurement and field name in Riemann service name.
|
||||
* `measurement_as_attribute`: Set measurement name as a Riemann attribute, instead of prepending it to the Riemann service name.
|
||||
* `string_as_state`: Send string metrics as Riemann event states. If this is not enabled then all string metrics will be ignored.
|
||||
* `tag_keys`: A list of tag keys whose values get sent as Riemann tags. If empty, all Telegraf tag values will be sent as tags.
|
||||
* `tags`: Additional Riemann tags that will be sent.
|
||||
* `description_text`: Description text for Riemann event.
|
||||
|
||||
### Example Events:
|
||||
|
||||
Riemann event emitted by Telegraf with default configuration:
|
||||
```
|
||||
#riemann.codec.Event{
|
||||
:host "postgresql-1e612b44-e92f-4d27-9f30-5e2f53947870", :state nil, :description nil, :ttl 30.0,
|
||||
:service "disk/used_percent", :metric 73.16736001949994, :path "/boot", :fstype "ext4", :time 1475605021}
|
||||
```
|
||||
|
||||
Telegraf emitting the same Riemann event with `measurement_as_attribute` set to `true`:
|
||||
```
|
||||
#riemann.codec.Event{ ...
|
||||
:measurement "disk", :service "used_percent", :metric 73.16736001949994,
|
||||
... :time 1475605021}
|
||||
```
|
||||
|
||||
Telegraf emitting the same Riemann event with additional Riemann tags defined:
|
||||
```
|
||||
#riemann.codec.Event{
|
||||
:host "postgresql-1e612b44-e92f-4d27-9f30-5e2f53947870", :state nil, :description nil, :ttl 30.0,
|
||||
:service "disk/used_percent", :metric 73.16736001949994, :path "/boot", :fstype "ext4", :time 1475605021,
|
||||
:tags ["telegraf" "postgres_cluster"]}
|
||||
```
|
||||
|
||||
Telegraf emitting a Riemann event with a status text and `string_as_state` set to `true`, and a `description_text` defined:
|
||||
```
|
||||
#riemann.codec.Event{
|
||||
:host "postgresql-1e612b44-e92f-4d27-9f30-5e2f53947870", :state "Running", :ttl 30.0,
|
||||
:description "PostgreSQL master node is up and running",
|
||||
:service "status", :time 1475605021}
|
||||
```
|
||||
@@ -3,6 +3,7 @@ package riemann
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -12,44 +13,70 @@ import (
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
)
|
||||
|
||||
const deprecationMsg = "I! WARNING: this Riemann output plugin will be deprecated in a future release, see https://github.com/influxdata/telegraf/issues/1878 for more details & discussion."
|
||||
|
||||
type Riemann struct {
|
||||
URL string
|
||||
Transport string
|
||||
Separator string
|
||||
URL string
|
||||
TTL float32
|
||||
Separator string
|
||||
MeasurementAsAttribute bool
|
||||
StringAsState bool
|
||||
TagKeys []string
|
||||
Tags []string
|
||||
DescriptionText string
|
||||
|
||||
client *raidman.Client
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
## URL of server
|
||||
url = "localhost:5555"
|
||||
## transport protocol to use either tcp or udp
|
||||
transport = "tcp"
|
||||
## separator to use between input name and field name in Riemann service name
|
||||
separator = " "
|
||||
## The full TCP or UDP URL of the Riemann server
|
||||
url = "tcp://localhost:5555"
|
||||
|
||||
## Riemann event TTL, floating-point time in seconds.
|
||||
## Defines how long that an event is considered valid for in Riemann
|
||||
# ttl = 30.0
|
||||
|
||||
## Separator to use between measurement and field name in Riemann service name
|
||||
## This does not have any effect if 'measurement_as_attribute' is set to 'true'
|
||||
separator = "/"
|
||||
|
||||
## Set measurement name as Riemann attribute 'measurement', instead of prepending it to the Riemann service name
|
||||
# measurement_as_attribute = false
|
||||
|
||||
## Send string metrics as Riemann event states.
|
||||
## Unless enabled all string metrics will be ignored
|
||||
# string_as_state = false
|
||||
|
||||
## A list of tag keys whose values get sent as Riemann tags.
|
||||
## If empty, all Telegraf tag values will be sent as tags
|
||||
# tag_keys = ["telegraf","custom_tag"]
|
||||
|
||||
## Additional Riemann tags to send.
|
||||
# tags = ["telegraf-output"]
|
||||
|
||||
## Description for Riemann event
|
||||
# description_text = "metrics collected from telegraf"
|
||||
`
|
||||
|
||||
func (r *Riemann) Connect() error {
|
||||
log.Printf(deprecationMsg)
|
||||
c, err := raidman.Dial(r.Transport, r.URL)
|
||||
parsed_url, err := url.Parse(r.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := raidman.Dial(parsed_url.Scheme, parsed_url.Host)
|
||||
if err != nil {
|
||||
r.client = nil
|
||||
return err
|
||||
}
|
||||
|
||||
r.client = c
|
||||
r.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Riemann) Close() error {
|
||||
if r.client == nil {
|
||||
return nil
|
||||
if r.client != nil {
|
||||
r.client.Close()
|
||||
r.client = nil
|
||||
}
|
||||
r.client.Close()
|
||||
r.client = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -62,91 +89,125 @@ func (r *Riemann) Description() string {
|
||||
}
|
||||
|
||||
func (r *Riemann) Write(metrics []telegraf.Metric) error {
|
||||
log.Printf(deprecationMsg)
|
||||
if len(metrics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.client == nil {
|
||||
err := r.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("FAILED to (re)connect to Riemann. Error: %s\n", err)
|
||||
if err := r.Connect(); err != nil {
|
||||
return fmt.Errorf("Failed to (re)connect to Riemann: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// build list of Riemann events to send
|
||||
var events []*raidman.Event
|
||||
for _, p := range metrics {
|
||||
evs := buildEvents(p, r.Separator)
|
||||
for _, m := range metrics {
|
||||
evs := r.buildRiemannEvents(m)
|
||||
for _, ev := range evs {
|
||||
events = append(events, ev)
|
||||
}
|
||||
}
|
||||
|
||||
var senderr = r.client.SendMulti(events)
|
||||
if senderr != nil {
|
||||
r.Close() // always retuns nil
|
||||
return fmt.Errorf("FAILED to send riemann message (will try to reconnect). Error: %s\n",
|
||||
senderr)
|
||||
if err := r.client.SendMulti(events); err != nil {
|
||||
r.Close()
|
||||
return fmt.Errorf("Failed to send riemann message: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildEvents(p telegraf.Metric, s string) []*raidman.Event {
|
||||
func (r *Riemann) buildRiemannEvents(m telegraf.Metric) []*raidman.Event {
|
||||
events := []*raidman.Event{}
|
||||
for fieldName, value := range p.Fields() {
|
||||
host, ok := p.Tags()["host"]
|
||||
for fieldName, value := range m.Fields() {
|
||||
// get host for Riemann event
|
||||
host, ok := m.Tags()["host"]
|
||||
if !ok {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
host = "unknown"
|
||||
} else {
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
host = hostname
|
||||
} else {
|
||||
host = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
event := &raidman.Event{
|
||||
Host: host,
|
||||
Service: serviceName(s, p.Name(), p.Tags(), fieldName),
|
||||
Host: host,
|
||||
Ttl: r.TTL,
|
||||
Description: r.DescriptionText,
|
||||
Time: m.Time().Unix(),
|
||||
|
||||
Attributes: r.attributes(m.Name(), m.Tags()),
|
||||
Service: r.service(m.Name(), fieldName),
|
||||
Tags: r.tags(m.Tags()),
|
||||
}
|
||||
|
||||
switch value.(type) {
|
||||
case string:
|
||||
// only send string metrics if explicitly enabled, skip otherwise
|
||||
if !r.StringAsState {
|
||||
log.Printf("D! Riemann event states disabled, skipping metric value [%s]\n", value)
|
||||
continue
|
||||
}
|
||||
event.State = value.(string)
|
||||
default:
|
||||
case int, int64, uint64, float32, float64:
|
||||
event.Metric = value
|
||||
default:
|
||||
log.Printf("D! Riemann does not support metric value [%s]\n", value)
|
||||
continue
|
||||
}
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
func serviceName(s string, n string, t map[string]string, f string) string {
|
||||
serviceStrings := []string{}
|
||||
serviceStrings = append(serviceStrings, n)
|
||||
|
||||
// we'll skip the 'host' tag
|
||||
tagStrings := []string{}
|
||||
tagNames := []string{}
|
||||
|
||||
for tagName := range t {
|
||||
tagNames = append(tagNames, tagName)
|
||||
func (r *Riemann) attributes(name string, tags map[string]string) map[string]string {
|
||||
if r.MeasurementAsAttribute {
|
||||
tags["measurement"] = name
|
||||
}
|
||||
sort.Strings(tagNames)
|
||||
|
||||
for _, tagName := range tagNames {
|
||||
if tagName != "host" {
|
||||
tagStrings = append(tagStrings, t[tagName])
|
||||
delete(tags, "host") // exclude 'host' tag
|
||||
return tags
|
||||
}
|
||||
|
||||
func (r *Riemann) service(name string, field string) string {
|
||||
var serviceStrings []string
|
||||
|
||||
// if measurement is not enabled as an attribute then prepend it to service name
|
||||
if !r.MeasurementAsAttribute {
|
||||
serviceStrings = append(serviceStrings, name)
|
||||
}
|
||||
serviceStrings = append(serviceStrings, field)
|
||||
|
||||
return strings.Join(serviceStrings, r.Separator)
|
||||
}
|
||||
|
||||
func (r *Riemann) tags(tags map[string]string) []string {
|
||||
// always add specified Riemann tags
|
||||
values := r.Tags
|
||||
|
||||
// if tag_keys are specified, add those and return tag list
|
||||
if len(r.TagKeys) > 0 {
|
||||
for _, tagName := range r.TagKeys {
|
||||
value, ok := tags[tagName]
|
||||
if ok {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// otherwise add all values from telegraf tag key/value pairs
|
||||
var keys []string
|
||||
for key := range tags {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
if key != "host" { // exclude 'host' tag
|
||||
values = append(values, tags[key])
|
||||
}
|
||||
}
|
||||
var tagString string = strings.Join(tagStrings, s)
|
||||
if tagString != "" {
|
||||
serviceStrings = append(serviceStrings, tagString)
|
||||
}
|
||||
serviceStrings = append(serviceStrings, f)
|
||||
return strings.Join(serviceStrings, s)
|
||||
return values
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,22 +1,181 @@
|
||||
package riemann
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/amir/raidman"
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAttributes(t *testing.T) {
|
||||
tags := map[string]string{"tag1": "value1", "tag2": "value2"}
|
||||
|
||||
r := &Riemann{}
|
||||
require.Equal(t,
|
||||
map[string]string{"tag1": "value1", "tag2": "value2"},
|
||||
r.attributes("test", tags))
|
||||
|
||||
// enable measurement as attribute, should now be included
|
||||
r.MeasurementAsAttribute = true
|
||||
require.Equal(t,
|
||||
map[string]string{"tag1": "value1", "tag2": "value2", "measurement": "test"},
|
||||
r.attributes("test", tags))
|
||||
}
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
r := &Riemann{
|
||||
Separator: "/",
|
||||
}
|
||||
require.Equal(t, "test/value", r.service("test", "value"))
|
||||
|
||||
// enable measurement as attribute, should not be part of service name anymore
|
||||
r.MeasurementAsAttribute = true
|
||||
require.Equal(t, "value", r.service("test", "value"))
|
||||
}
|
||||
|
||||
func TestTags(t *testing.T) {
|
||||
tags := map[string]string{"tag1": "value1", "tag2": "value2"}
|
||||
|
||||
// all tag values plus additional tag should be present
|
||||
r := &Riemann{
|
||||
Tags: []string{"test"},
|
||||
}
|
||||
require.Equal(t,
|
||||
[]string{"test", "value1", "value2"},
|
||||
r.tags(tags))
|
||||
|
||||
// only tag2 value plus additional tag should be present
|
||||
r.TagKeys = []string{"tag2"}
|
||||
require.Equal(t,
|
||||
[]string{"test", "value2"},
|
||||
r.tags(tags))
|
||||
|
||||
// only tag1 value should be present
|
||||
r.Tags = nil
|
||||
r.TagKeys = []string{"tag1"}
|
||||
require.Equal(t,
|
||||
[]string{"value1"},
|
||||
r.tags(tags))
|
||||
}
|
||||
|
||||
func TestMetricEvents(t *testing.T) {
|
||||
r := &Riemann{
|
||||
TTL: 20.0,
|
||||
Separator: "/",
|
||||
MeasurementAsAttribute: false,
|
||||
DescriptionText: "metrics from telegraf",
|
||||
Tags: []string{"telegraf"},
|
||||
}
|
||||
|
||||
// build a single event
|
||||
m, _ := metric.New(
|
||||
"test1",
|
||||
map[string]string{"tag1": "value1", "host": "abc123"},
|
||||
map[string]interface{}{"value": 5.6},
|
||||
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
events := r.buildRiemannEvents(m)
|
||||
require.Len(t, events, 1)
|
||||
|
||||
// is event as expected?
|
||||
expectedEvent := &raidman.Event{
|
||||
Ttl: 20.0,
|
||||
Time: 1257894000,
|
||||
Tags: []string{"telegraf", "value1"},
|
||||
Host: "abc123",
|
||||
State: "",
|
||||
Service: "test1/value",
|
||||
Metric: 5.6,
|
||||
Description: "metrics from telegraf",
|
||||
Attributes: map[string]string{"tag1": "value1"},
|
||||
}
|
||||
require.Equal(t, expectedEvent, events[0])
|
||||
|
||||
// build 2 events
|
||||
m, _ = metric.New(
|
||||
"test2",
|
||||
map[string]string{"host": "xyz987"},
|
||||
map[string]interface{}{"point": 1},
|
||||
time.Date(2012, time.November, 2, 3, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
events = append(events, r.buildRiemannEvents(m)...)
|
||||
require.Len(t, events, 2)
|
||||
|
||||
// first event should still be the same
|
||||
require.Equal(t, expectedEvent, events[0])
|
||||
|
||||
// second event
|
||||
expectedEvent = &raidman.Event{
|
||||
Ttl: 20.0,
|
||||
Time: 1351825200,
|
||||
Tags: []string{"telegraf"},
|
||||
Host: "xyz987",
|
||||
State: "",
|
||||
Service: "test2/point",
|
||||
Metric: int64(1),
|
||||
Description: "metrics from telegraf",
|
||||
Attributes: map[string]string{},
|
||||
}
|
||||
require.Equal(t, expectedEvent, events[1])
|
||||
}
|
||||
|
||||
func TestStateEvents(t *testing.T) {
|
||||
r := &Riemann{
|
||||
MeasurementAsAttribute: true,
|
||||
}
|
||||
|
||||
// string metrics will be skipped unless explicitly enabled
|
||||
m, _ := metric.New(
|
||||
"test",
|
||||
map[string]string{"host": "host"},
|
||||
map[string]interface{}{"value": "running"},
|
||||
time.Date(2015, time.November, 9, 22, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
events := r.buildRiemannEvents(m)
|
||||
// no event should be present
|
||||
require.Len(t, events, 0)
|
||||
|
||||
// enable string metrics as event states
|
||||
r.StringAsState = true
|
||||
events = r.buildRiemannEvents(m)
|
||||
require.Len(t, events, 1)
|
||||
|
||||
// is event as expected?
|
||||
expectedEvent := &raidman.Event{
|
||||
Ttl: 0,
|
||||
Time: 1447106400,
|
||||
Tags: nil,
|
||||
Host: "host",
|
||||
State: "running",
|
||||
Service: "value",
|
||||
Metric: nil,
|
||||
Description: "",
|
||||
Attributes: map[string]string{"measurement": "test"},
|
||||
}
|
||||
require.Equal(t, expectedEvent, events[0])
|
||||
}
|
||||
|
||||
func TestConnectAndWrite(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
url := testutil.GetLocalHost() + ":5555"
|
||||
|
||||
r := &Riemann{
|
||||
URL: url,
|
||||
Transport: "tcp",
|
||||
URL: fmt.Sprintf("tcp://%s:5555", testutil.GetLocalHost()),
|
||||
TTL: 15.0,
|
||||
Separator: "/",
|
||||
MeasurementAsAttribute: false,
|
||||
StringAsState: true,
|
||||
DescriptionText: "metrics from telegraf",
|
||||
Tags: []string{"docker"},
|
||||
}
|
||||
|
||||
err := r.Connect()
|
||||
@@ -24,4 +183,32 @@ func TestConnectAndWrite(t *testing.T) {
|
||||
|
||||
err = r.Write(testutil.MockMetrics())
|
||||
require.NoError(t, err)
|
||||
|
||||
metrics := make([]telegraf.Metric, 0)
|
||||
metrics = append(metrics, testutil.TestMetric(2))
|
||||
metrics = append(metrics, testutil.TestMetric(3.456789))
|
||||
metrics = append(metrics, testutil.TestMetric(uint(0)))
|
||||
metrics = append(metrics, testutil.TestMetric("ok"))
|
||||
metrics = append(metrics, testutil.TestMetric("running"))
|
||||
err = r.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// are there any "docker" tagged events in Riemann?
|
||||
events, err := r.client.Query(`tagged "docker"`)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, len(events))
|
||||
|
||||
// get Riemann events with state = "running", should be 1 event
|
||||
events, err = r.client.Query(`state = "running"`)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, events, 1)
|
||||
|
||||
// is event as expected?
|
||||
require.Equal(t, []string{"docker", "value1"}, events[0].Tags)
|
||||
require.Equal(t, "running", events[0].State)
|
||||
require.Equal(t, "test1/value", events[0].Service)
|
||||
require.Equal(t, "metrics from telegraf", events[0].Description)
|
||||
require.Equal(t, map[string]string{"tag1": "value1"}, events[0].Attributes)
|
||||
}
|
||||
|
||||
156
plugins/outputs/riemann_legacy/riemann.go
Normal file
156
plugins/outputs/riemann_legacy/riemann.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package riemann_legacy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/amir/raidman"
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
)
|
||||
|
||||
const deprecationMsg = "E! Error: this Riemann output plugin will be deprecated in a future release, see https://github.com/influxdata/telegraf/issues/1878 for more details & discussion."
|
||||
|
||||
type Riemann struct {
|
||||
URL string
|
||||
Transport string
|
||||
Separator string
|
||||
|
||||
client *raidman.Client
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
## URL of server
|
||||
url = "localhost:5555"
|
||||
## transport protocol to use either tcp or udp
|
||||
transport = "tcp"
|
||||
## separator to use between input name and field name in Riemann service name
|
||||
separator = " "
|
||||
`
|
||||
|
||||
func (r *Riemann) Connect() error {
|
||||
log.Printf(deprecationMsg)
|
||||
c, err := raidman.Dial(r.Transport, r.URL)
|
||||
|
||||
if err != nil {
|
||||
r.client = nil
|
||||
return err
|
||||
}
|
||||
|
||||
r.client = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Riemann) Close() error {
|
||||
if r.client == nil {
|
||||
return nil
|
||||
}
|
||||
r.client.Close()
|
||||
r.client = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Riemann) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (r *Riemann) Description() string {
|
||||
return "Configuration for the Riemann server to send metrics to"
|
||||
}
|
||||
|
||||
func (r *Riemann) Write(metrics []telegraf.Metric) error {
|
||||
log.Printf(deprecationMsg)
|
||||
if len(metrics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.client == nil {
|
||||
err := r.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("FAILED to (re)connect to Riemann. Error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
var events []*raidman.Event
|
||||
for _, p := range metrics {
|
||||
evs := buildEvents(p, r.Separator)
|
||||
for _, ev := range evs {
|
||||
events = append(events, ev)
|
||||
}
|
||||
}
|
||||
|
||||
var senderr = r.client.SendMulti(events)
|
||||
if senderr != nil {
|
||||
r.Close() // always retuns nil
|
||||
return fmt.Errorf("FAILED to send riemann message (will try to reconnect). Error: %s\n",
|
||||
senderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildEvents(p telegraf.Metric, s string) []*raidman.Event {
|
||||
events := []*raidman.Event{}
|
||||
for fieldName, value := range p.Fields() {
|
||||
host, ok := p.Tags()["host"]
|
||||
if !ok {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
host = "unknown"
|
||||
} else {
|
||||
host = hostname
|
||||
}
|
||||
}
|
||||
|
||||
event := &raidman.Event{
|
||||
Host: host,
|
||||
Service: serviceName(s, p.Name(), p.Tags(), fieldName),
|
||||
}
|
||||
|
||||
switch value.(type) {
|
||||
case string:
|
||||
event.State = value.(string)
|
||||
default:
|
||||
event.Metric = value
|
||||
}
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
func serviceName(s string, n string, t map[string]string, f string) string {
|
||||
serviceStrings := []string{}
|
||||
serviceStrings = append(serviceStrings, n)
|
||||
|
||||
// we'll skip the 'host' tag
|
||||
tagStrings := []string{}
|
||||
tagNames := []string{}
|
||||
|
||||
for tagName := range t {
|
||||
tagNames = append(tagNames, tagName)
|
||||
}
|
||||
sort.Strings(tagNames)
|
||||
|
||||
for _, tagName := range tagNames {
|
||||
if tagName != "host" {
|
||||
tagStrings = append(tagStrings, t[tagName])
|
||||
}
|
||||
}
|
||||
var tagString string = strings.Join(tagStrings, s)
|
||||
if tagString != "" {
|
||||
serviceStrings = append(serviceStrings, tagString)
|
||||
}
|
||||
serviceStrings = append(serviceStrings, f)
|
||||
return strings.Join(serviceStrings, s)
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("riemann_legacy", func() telegraf.Output {
|
||||
return &Riemann{}
|
||||
})
|
||||
}
|
||||
27
plugins/outputs/riemann_legacy/riemann_test.go
Normal file
27
plugins/outputs/riemann_legacy/riemann_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package riemann_legacy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConnectAndWrite(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
url := testutil.GetLocalHost() + ":5555"
|
||||
|
||||
r := &Riemann{
|
||||
URL: url,
|
||||
Transport: "tcp",
|
||||
}
|
||||
|
||||
err := r.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.Write(testutil.MockMetrics())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
106
plugins/outputs/socket_writer/socket_writer.go
Normal file
106
plugins/outputs/socket_writer/socket_writer.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package socket_writer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
"github.com/influxdata/telegraf/plugins/serializers"
|
||||
)
|
||||
|
||||
type SocketWriter struct {
|
||||
Address string
|
||||
|
||||
serializers.Serializer
|
||||
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (sw *SocketWriter) Description() string {
|
||||
return "Generic socket writer capable of handling multiple socket types."
|
||||
}
|
||||
|
||||
func (sw *SocketWriter) SampleConfig() string {
|
||||
return `
|
||||
## URL to connect to
|
||||
# address = "tcp://127.0.0.1:8094"
|
||||
# address = "tcp://example.com:http"
|
||||
# address = "tcp4://127.0.0.1:8094"
|
||||
# address = "tcp6://127.0.0.1:8094"
|
||||
# address = "tcp6://[2001:db8::1]:8094"
|
||||
# address = "udp://127.0.0.1:8094"
|
||||
# address = "udp4://127.0.0.1:8094"
|
||||
# address = "udp6://127.0.0.1:8094"
|
||||
# address = "unix:///tmp/telegraf.sock"
|
||||
# address = "unixgram:///tmp/telegraf.sock"
|
||||
|
||||
## Data format to generate.
|
||||
## Each data format has it's own unique set of configuration options, read
|
||||
## more about them here:
|
||||
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md
|
||||
# data_format = "influx"
|
||||
`
|
||||
}
|
||||
|
||||
func (sw *SocketWriter) SetSerializer(s serializers.Serializer) {
|
||||
sw.Serializer = s
|
||||
}
|
||||
|
||||
func (sw *SocketWriter) Connect() error {
|
||||
spl := strings.SplitN(sw.Address, "://", 2)
|
||||
if len(spl) != 2 {
|
||||
return fmt.Errorf("invalid address: %s", sw.Address)
|
||||
}
|
||||
|
||||
c, err := net.Dial(spl[0], spl[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sw.Conn = c
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes the given metrics to the destination.
|
||||
// If an error is encountered, it is up to the caller to retry the same write again later.
|
||||
// Not parallel safe.
|
||||
func (sw *SocketWriter) Write(metrics []telegraf.Metric) error {
|
||||
if sw.Conn == nil {
|
||||
// previous write failed with permanent error and socket was closed.
|
||||
if err := sw.Connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range metrics {
|
||||
bs, err := sw.Serialize(m)
|
||||
if err != nil {
|
||||
//TODO log & keep going with remaining metrics
|
||||
return err
|
||||
}
|
||||
if _, err := sw.Conn.Write(bs); err != nil {
|
||||
//TODO log & keep going with remaining strings
|
||||
if err, ok := err.(net.Error); !ok || !err.Temporary() {
|
||||
// permanent error. close the connection
|
||||
sw.Close()
|
||||
sw.Conn = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newSocketWriter() *SocketWriter {
|
||||
s, _ := serializers.NewInfluxSerializer()
|
||||
return &SocketWriter{
|
||||
Serializer: s,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("socket_writer", func() telegraf.Output { return newSocketWriter() })
|
||||
}
|
||||
187
plugins/outputs/socket_writer/socket_writer_test.go
Normal file
187
plugins/outputs/socket_writer/socket_writer_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package socket_writer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSocketWriter_tcp(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
sw := newSocketWriter()
|
||||
sw.Address = "tcp://" + listener.Addr().String()
|
||||
|
||||
err = sw.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
lconn, err := listener.Accept()
|
||||
require.NoError(t, err)
|
||||
|
||||
testSocketWriter_stream(t, sw, lconn)
|
||||
}
|
||||
|
||||
func TestSocketWriter_udp(t *testing.T) {
|
||||
listener, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
sw := newSocketWriter()
|
||||
sw.Address = "udp://" + listener.LocalAddr().String()
|
||||
|
||||
err = sw.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
testSocketWriter_packet(t, sw, listener)
|
||||
}
|
||||
|
||||
func TestSocketWriter_unix(t *testing.T) {
|
||||
defer os.Remove("/tmp/telegraf_test.sock")
|
||||
listener, err := net.Listen("unix", "/tmp/telegraf_test.sock")
|
||||
require.NoError(t, err)
|
||||
|
||||
sw := newSocketWriter()
|
||||
sw.Address = "unix:///tmp/telegraf_test.sock"
|
||||
|
||||
err = sw.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
lconn, err := listener.Accept()
|
||||
require.NoError(t, err)
|
||||
|
||||
testSocketWriter_stream(t, sw, lconn)
|
||||
}
|
||||
|
||||
func TestSocketWriter_unixgram(t *testing.T) {
|
||||
defer os.Remove("/tmp/telegraf_test.sock")
|
||||
listener, err := net.ListenPacket("unixgram", "/tmp/telegraf_test.sock")
|
||||
require.NoError(t, err)
|
||||
|
||||
sw := newSocketWriter()
|
||||
sw.Address = "unixgram:///tmp/telegraf_test.sock"
|
||||
|
||||
err = sw.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
testSocketWriter_packet(t, sw, listener)
|
||||
}
|
||||
|
||||
func testSocketWriter_stream(t *testing.T, sw *SocketWriter, lconn net.Conn) {
|
||||
metrics := []telegraf.Metric{}
|
||||
metrics = append(metrics, testutil.TestMetric(1, "test"))
|
||||
mbs1out, _ := sw.Serialize(metrics[0])
|
||||
metrics = append(metrics, testutil.TestMetric(2, "test"))
|
||||
mbs2out, _ := sw.Serialize(metrics[1])
|
||||
|
||||
err := sw.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
scnr := bufio.NewScanner(lconn)
|
||||
require.True(t, scnr.Scan())
|
||||
mstr1in := scnr.Text() + "\n"
|
||||
require.True(t, scnr.Scan())
|
||||
mstr2in := scnr.Text() + "\n"
|
||||
|
||||
assert.Equal(t, string(mbs1out), mstr1in)
|
||||
assert.Equal(t, string(mbs2out), mstr2in)
|
||||
}
|
||||
|
||||
func testSocketWriter_packet(t *testing.T, sw *SocketWriter, lconn net.PacketConn) {
|
||||
metrics := []telegraf.Metric{}
|
||||
metrics = append(metrics, testutil.TestMetric(1, "test"))
|
||||
mbs1out, _ := sw.Serialize(metrics[0])
|
||||
metrics = append(metrics, testutil.TestMetric(2, "test"))
|
||||
mbs2out, _ := sw.Serialize(metrics[1])
|
||||
|
||||
err := sw.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
buf := make([]byte, 256)
|
||||
var mstrins []string
|
||||
for len(mstrins) < 2 {
|
||||
n, _, err := lconn.ReadFrom(buf)
|
||||
require.NoError(t, err)
|
||||
for _, bs := range bytes.Split(buf[:n], []byte{'\n'}) {
|
||||
if len(bs) == 0 {
|
||||
continue
|
||||
}
|
||||
mstrins = append(mstrins, string(bs)+"\n")
|
||||
}
|
||||
}
|
||||
require.Len(t, mstrins, 2)
|
||||
|
||||
assert.Equal(t, string(mbs1out), mstrins[0])
|
||||
assert.Equal(t, string(mbs2out), mstrins[1])
|
||||
}
|
||||
|
||||
func TestSocketWriter_Write_err(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
sw := newSocketWriter()
|
||||
sw.Address = "tcp://" + listener.Addr().String()
|
||||
|
||||
err = sw.Connect()
|
||||
require.NoError(t, err)
|
||||
sw.Conn.(*net.TCPConn).SetReadBuffer(256)
|
||||
|
||||
lconn, err := listener.Accept()
|
||||
require.NoError(t, err)
|
||||
lconn.(*net.TCPConn).SetWriteBuffer(256)
|
||||
|
||||
metrics := []telegraf.Metric{testutil.TestMetric(1, "testerr")}
|
||||
|
||||
// close the socket to generate an error
|
||||
lconn.Close()
|
||||
sw.Close()
|
||||
err = sw.Write(metrics)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, sw.Conn)
|
||||
}
|
||||
|
||||
func TestSocketWriter_Write_reconnect(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
sw := newSocketWriter()
|
||||
sw.Address = "tcp://" + listener.Addr().String()
|
||||
|
||||
err = sw.Connect()
|
||||
require.NoError(t, err)
|
||||
sw.Conn.(*net.TCPConn).SetReadBuffer(256)
|
||||
|
||||
lconn, err := listener.Accept()
|
||||
require.NoError(t, err)
|
||||
lconn.(*net.TCPConn).SetWriteBuffer(256)
|
||||
lconn.Close()
|
||||
sw.Conn = nil
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
var lerr error
|
||||
go func() {
|
||||
lconn, lerr = listener.Accept()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
metrics := []telegraf.Metric{testutil.TestMetric(1, "testerr")}
|
||||
err = sw.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
wg.Wait()
|
||||
assert.NoError(t, lerr)
|
||||
|
||||
mbsout, _ := sw.Serialize(metrics[0])
|
||||
buf := make([]byte, 256)
|
||||
n, err := lconn.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(mbsout), string(buf[:n]))
|
||||
}
|
||||
@@ -19,6 +19,7 @@ var (
|
||||
|
||||
const (
|
||||
validInflux = "cpu_load_short,cpu=cpu0 value=10 1257894000000000000\n"
|
||||
negativeFloat = "cpu_load_short,cpu=cpu0 value=-13.4 1257894000000000000\n"
|
||||
validInfluxNewline = "\ncpu_load_short,cpu=cpu0 value=10 1257894000000000000\n"
|
||||
validInfluxNoNewline = "cpu_load_short,cpu=cpu0 value=10 1257894000000000000"
|
||||
invalidInflux = "I don't think this is line protocol\n"
|
||||
@@ -82,6 +83,18 @@ func TestParseValidInflux(t *testing.T) {
|
||||
"cpu": "cpu0",
|
||||
}, metrics[0].Tags())
|
||||
assert.Equal(t, exptime, metrics[0].Time().UnixNano())
|
||||
|
||||
metrics, err = parser.Parse([]byte(negativeFloat))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, metrics, 1)
|
||||
assert.Equal(t, "cpu_load_short", metrics[0].Name())
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"value": float64(-13.4),
|
||||
}, metrics[0].Fields())
|
||||
assert.Equal(t, map[string]string{
|
||||
"cpu": "cpu0",
|
||||
}, metrics[0].Tags())
|
||||
assert.Equal(t, exptime, metrics[0].Time().UnixNano())
|
||||
}
|
||||
|
||||
func TestParseLineValidInflux(t *testing.T) {
|
||||
|
||||
@@ -83,9 +83,9 @@ targets = {
|
||||
}
|
||||
|
||||
supported_builds = {
|
||||
"windows": [ "amd64" ],
|
||||
"windows": [ "amd64", "i386" ],
|
||||
"linux": [ "amd64", "i386", "armhf", "armel", "arm64", "static_amd64" ],
|
||||
"freebsd": [ "amd64" ]
|
||||
"freebsd": [ "amd64", "i386" ]
|
||||
}
|
||||
|
||||
supported_packages = {
|
||||
|
||||
@@ -26,7 +26,12 @@ function install_chkconfig {
|
||||
|
||||
id telegraf &>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
useradd -r -K USERGROUPS_ENAB=yes -M telegraf -s /bin/false -d /etc/telegraf
|
||||
grep "^telegraf:" /etc/group &>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
useradd -r -K USERGROUPS_ENAB=yes -M telegraf -s /bin/false -d /etc/telegraf
|
||||
else
|
||||
useradd -r -K USERGROUPS_ENAB=yes -M telegraf -s /bin/false -d /etc/telegraf -g telegraf
|
||||
fi
|
||||
fi
|
||||
|
||||
test -d $LOG_DIR || mkdir -p $LOG_DIR
|
||||
|
||||
@@ -29,6 +29,7 @@ func (p *Metric) String() string {
|
||||
// Accumulator defines a mocked out accumulator
|
||||
type Accumulator struct {
|
||||
sync.Mutex
|
||||
*sync.Cond
|
||||
|
||||
Metrics []*Metric
|
||||
nMetrics uint64
|
||||
@@ -56,11 +57,14 @@ func (a *Accumulator) AddFields(
|
||||
timestamp ...time.Time,
|
||||
) {
|
||||
atomic.AddUint64(&a.nMetrics, 1)
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
if a.Cond != nil {
|
||||
a.Cond.Broadcast()
|
||||
}
|
||||
if a.Discard {
|
||||
return
|
||||
}
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
if tags == nil {
|
||||
tags = map[string]string{}
|
||||
}
|
||||
@@ -171,6 +175,15 @@ func (a *Accumulator) NFields() int {
|
||||
return counter
|
||||
}
|
||||
|
||||
// Wait waits for a metric to be added to the accumulator.
|
||||
// Accumulator must already be locked.
|
||||
func (a *Accumulator) Wait() {
|
||||
if a.Cond == nil {
|
||||
a.Cond = sync.NewCond(&a.Mutex)
|
||||
}
|
||||
a.Cond.Wait()
|
||||
}
|
||||
|
||||
func (a *Accumulator) AssertContainsTaggedFields(
|
||||
t *testing.T,
|
||||
measurement string,
|
||||
@@ -221,7 +234,7 @@ func (a *Accumulator) AssertDoesNotContainMeasurement(t *testing.T, measurement
|
||||
}
|
||||
}
|
||||
|
||||
// HasIntValue returns true if the measurement has an Int value
|
||||
// HasIntField returns true if the measurement has an Int value
|
||||
func (a *Accumulator) HasIntField(measurement string, field string) bool {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
@@ -239,6 +252,42 @@ func (a *Accumulator) HasIntField(measurement string, field string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HasInt32Field returns true if the measurement has an Int value
|
||||
func (a *Accumulator) HasInt32Field(measurement string, field string) bool {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
for _, p := range a.Metrics {
|
||||
if p.Measurement == measurement {
|
||||
for fieldname, value := range p.Fields {
|
||||
if fieldname == field {
|
||||
_, ok := value.(int32)
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasStringField returns true if the measurement has an String value
|
||||
func (a *Accumulator) HasStringField(measurement string, field string) bool {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
for _, p := range a.Metrics {
|
||||
if p.Measurement == measurement {
|
||||
for fieldname, value := range p.Fields {
|
||||
if fieldname == field {
|
||||
_, ok := value.(string)
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasUIntValue returns true if the measurement has a UInt value
|
||||
func (a *Accumulator) HasUIntField(measurement string, field string) bool {
|
||||
a.Lock()
|
||||
|
||||
Reference in New Issue
Block a user