feat(parsers): Add binary parser (#11552)
This commit is contained in:
parent
99a48f460e
commit
393566d9ba
|
|
@ -5,6 +5,7 @@ using a configurable parser into [metrics][]. This allows, for example, the
|
||||||
`kafka_consumer` input plugin to process messages in either InfluxDB Line
|
`kafka_consumer` input plugin to process messages in either InfluxDB Line
|
||||||
Protocol or in JSON format.
|
Protocol or in JSON format.
|
||||||
|
|
||||||
|
- [Binary](/plugins/parsers/binary)
|
||||||
- [Collectd](/plugins/parsers/collectd)
|
- [Collectd](/plugins/parsers/collectd)
|
||||||
- [CSV](/plugins/parsers/csv)
|
- [CSV](/plugins/parsers/csv)
|
||||||
- [Dropwizard](/plugins/parsers/dropwizard)
|
- [Dropwizard](/plugins/parsers/dropwizard)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
//go:build !custom || parsers || parsers.binary
|
||||||
|
|
||||||
|
package all
|
||||||
|
|
||||||
|
import _ "github.com/influxdata/telegraf/plugins/parsers/binary" // register plugin
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
# Binary Parser Plugin
|
||||||
|
|
||||||
|
The `binary` data format parser parses binary protocols into metrics using
|
||||||
|
user-specified configurations.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[inputs.file]]
|
||||||
|
files = ["example.bin"]
|
||||||
|
|
||||||
|
## Data format to consume.
|
||||||
|
## Each data format has its 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 = "binary"
|
||||||
|
|
||||||
|
## Do not error-out if none of the filter expressions below matches.
|
||||||
|
# allow_no_match = false
|
||||||
|
|
||||||
|
## Specify the endianess of the data.
|
||||||
|
## Available values are "be" (big-endian), "le" (little-endian) and "host",
|
||||||
|
## where "host" means the same endianess as the machine running Telegraf.
|
||||||
|
# endianess = "host"
|
||||||
|
|
||||||
|
## Multiple parsing sections are allowed
|
||||||
|
[[inputs.file.binary]]
|
||||||
|
## Optional: Metric (measurement) name to use if not extracted from the data.
|
||||||
|
# metric_name = "my_name"
|
||||||
|
|
||||||
|
## Definition of the message format and the extracted data.
|
||||||
|
## Please note that you need to define all elements of the data in the
|
||||||
|
## correct order with the correct length as the data is parsed in the order
|
||||||
|
## given.
|
||||||
|
## An entry can have the following properties:
|
||||||
|
## name -- Name of the element (e.g. field or tag). Can be omitted
|
||||||
|
## for special assignments (i.e. time & measurement) or if
|
||||||
|
## entry is omitted.
|
||||||
|
## type -- Data-type of the entry. Can be "int8/16/32/64", "uint8/16/32/64",
|
||||||
|
## "float32/64", "bool" and "string".
|
||||||
|
## In case of time, this can be any of "unix" (default), "unix_ms", "unix_us",
|
||||||
|
## "unix_ns" or a valid Golang time format.
|
||||||
|
## bits -- Length in bits for this entry. If omitted, the length derived from
|
||||||
|
## the "type" property will be used. For "time" 64-bit will be used
|
||||||
|
## as default.
|
||||||
|
## assignment -- Assignment of the gathered data. Can be "measurement", "time",
|
||||||
|
## "field" or "tag". If omitted "field" is assumed.
|
||||||
|
## omit -- Omit the given data. If true, the data is skipped and not added
|
||||||
|
## to the metric. Omitted entries only need a length definition
|
||||||
|
## via "bits" or "type".
|
||||||
|
## terminator -- Terminator for dynamic-length strings. Only used for "string" type.
|
||||||
|
## Valid values are "fixed" (fixed length string given by "bits"),
|
||||||
|
## "null" (null-terminated string) or a character sequence specified
|
||||||
|
## as HEX values (e.g. "0x0D0A"). Defaults to "fixed" for strings.
|
||||||
|
## timezone -- Timezone of "time" entries. Only applies to "time" assignments.
|
||||||
|
## Can be "utc", "local" or any valid Golang timezone (e.g. "Europe/Berlin")
|
||||||
|
entries = [
|
||||||
|
{ type = "string", assignment = "measurement", terminator: "null" },
|
||||||
|
{ name = "address", type = "uint16", assignment = "tag" },
|
||||||
|
{ name = "value", type = "float64" },
|
||||||
|
{ type = "unix", assignment = "time" },
|
||||||
|
]
|
||||||
|
|
||||||
|
## Optional: Filter evaluated before applying the configuration.
|
||||||
|
## This option can be used to mange multiple configuration specific for
|
||||||
|
## a certain message type. If no filter is given, the configuration is applied.
|
||||||
|
# [inputs.file.binary.filter]
|
||||||
|
# ## Filter message by the exact length in bytes (default: N/A).
|
||||||
|
# # length = 0
|
||||||
|
# ## Filter the message by a minimum length in bytes.
|
||||||
|
# ## Messages longer of of equal length will pass.
|
||||||
|
# # length_min = 0
|
||||||
|
# ## List of data parts to match.
|
||||||
|
# ## Only if all selected parts match, the configuration will be
|
||||||
|
# ## applied. The "offset" is the start of the data to match in bits,
|
||||||
|
# ## "bits" is the length in bits and "match" is the value to match
|
||||||
|
# ## against. Non-byte boundaries are supported, data is always right-aligned.
|
||||||
|
# selection = [
|
||||||
|
# { offset = 0, bits = 8, match = "0x1F" },
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
```
|
||||||
|
|
||||||
|
In this configuration mode, you explicitly specify the field and tags you want
|
||||||
|
to scrape out of your data.
|
||||||
|
|
||||||
|
A configuration can contain multiple `binary` subsections for e.g. the file
|
||||||
|
plugin to process the binary data multiple times. This can be useful
|
||||||
|
(together with _filters_) to handle different message types.
|
||||||
|
|
||||||
|
__Please note__: The `filter` section needs to be placed __after__ the `entries`
|
||||||
|
definitions due to TOML constraints as otherwise the entries will be assigned
|
||||||
|
to the filter section.
|
||||||
|
|
||||||
|
### General options and remarks
|
||||||
|
|
||||||
|
#### `allow_no_match` (optional)
|
||||||
|
|
||||||
|
By specifying `allow_no_match` you allow the parser to silently ignore data
|
||||||
|
that does not match _any_ given configuration filter. This can be useful if
|
||||||
|
you only want to collect a subset of the available messages.
|
||||||
|
|
||||||
|
#### `endianess` (optional)
|
||||||
|
|
||||||
|
This specifies the endianess of the data. If not specified, the parser will
|
||||||
|
fallback to the "host" endianess, assuming that the message and Telegraf
|
||||||
|
machine share the same endianess.
|
||||||
|
Alternatively, you can explicitly specify big-endian format (`"be"`) or
|
||||||
|
little-endian format (`"le"`).
|
||||||
|
|
||||||
|
### Non-byte aligned value extraction
|
||||||
|
|
||||||
|
In both, `filter` and `entries` definitions, values can be extracted at non-byte
|
||||||
|
boundaries. You can for example extract 3-bit starting at bit-offset 8. In those
|
||||||
|
cases, the result will be masked and shifted such that the resulting byte-value
|
||||||
|
is _right_ aligned. In case your 3-bit are `101` the resulting byte value is
|
||||||
|
`0x05`.
|
||||||
|
|
||||||
|
This is especially important when specifying the `match` value in the filter
|
||||||
|
section.
|
||||||
|
|
||||||
|
### Entries definitions
|
||||||
|
|
||||||
|
The entry array specifies how to dissect the message into the measurement name,
|
||||||
|
the timestamp, tags and fields.
|
||||||
|
|
||||||
|
#### `measurement` specification
|
||||||
|
|
||||||
|
When setting the `assignment` to `"measurement"`, the extracted value
|
||||||
|
will be used as the metric name, overriding other specifications.
|
||||||
|
The `type` setting is assumed to be `"string"` and can be omitted similar
|
||||||
|
to the `name` option. See [`string` type handling](#string-type-handling)
|
||||||
|
for details and further options.
|
||||||
|
|
||||||
|
### `time` specification
|
||||||
|
|
||||||
|
When setting the `assignment` to `"time"`, the extracted value
|
||||||
|
will be used as the timestamp of the metric. By default the current
|
||||||
|
time will be used for all created metrics.
|
||||||
|
|
||||||
|
The `type` setting here contains the time-format can be set to `unix`,
|
||||||
|
`unix_ms`, `unix_us`, `unix_ns`, or an accepted
|
||||||
|
[Go "reference time"][time const]. Consult the Go [time][time parse]
|
||||||
|
package for details and additional examples on how to set the time format.
|
||||||
|
If `type` is omitted the `unix` format is assumed.
|
||||||
|
|
||||||
|
For the `unix` format and derivatives, the underlying value is assumed
|
||||||
|
to be a 64-bit integer. The `bits` setting can be used to specify other
|
||||||
|
length settings. All other time-formats assume a fixed-length `string`
|
||||||
|
value to be extracted. The length of the string is automatically
|
||||||
|
determined using the format setting in `type`.
|
||||||
|
|
||||||
|
The `timezone` setting allows to convert the extracted time to the
|
||||||
|
given value timezone. By default the time will be interpreted as `utc`.
|
||||||
|
Other valid values are `local`, i.e. the local timezone configured for
|
||||||
|
the machine, or valid timezone-specification e.g. `Europe/Berlin`.
|
||||||
|
|
||||||
|
### `tag` specification
|
||||||
|
|
||||||
|
When setting the `assignment` to `"tag"`, the extracted value
|
||||||
|
will be used as a tag. The `name` setting will be the name of the tag
|
||||||
|
and the `type` will default to `string`. When specifying other types,
|
||||||
|
the extracted value will first be interpreted as the given type and
|
||||||
|
then converted to `string`.
|
||||||
|
|
||||||
|
The `bits` setting can be used to specify the length of the data to
|
||||||
|
extract and is required for fixed-length `string` types.
|
||||||
|
|
||||||
|
### `field` specification
|
||||||
|
|
||||||
|
When setting the `assignment` to `"field"` or omitting the `assignment`
|
||||||
|
setting, the extracted value will be used as a field. The `name` setting
|
||||||
|
is used as the name of the field and the `type` as type of the field value.
|
||||||
|
|
||||||
|
The `bits` setting can be used to specify the length of the data to
|
||||||
|
extract. By default the length corresponding to `type` is used.
|
||||||
|
Please see the [string](#string-type-handling) and [bool](#bool-type-handling)
|
||||||
|
specific sections when using those types.
|
||||||
|
|
||||||
|
### `string` type handling
|
||||||
|
|
||||||
|
Strings are assumed to be fixed-length strings by default. In this case, the
|
||||||
|
`bits` setting is mandatory to specify the length of the string in _bit_.
|
||||||
|
|
||||||
|
To handle dynamic strings, the `terminator` setting can be used to specify
|
||||||
|
characters to terminate the string. The two named options, `fixed` and `null`
|
||||||
|
will specify fixed-length and null-terminated strings, respectively.
|
||||||
|
Any other setting will be interpreted as hexadecimal sequence of bytes
|
||||||
|
matching the end of the string. The termination-sequence is removed from
|
||||||
|
the result.
|
||||||
|
|
||||||
|
### `bool` type handling
|
||||||
|
|
||||||
|
By default `bool` types are assumed to be _one_ bit in length. You can
|
||||||
|
specify any other length by using the `bits` setting.
|
||||||
|
When interpreting values as booleans, any zero value will be `false`,
|
||||||
|
while any non-zero value will result in `true`.
|
||||||
|
|
||||||
|
### omitting data
|
||||||
|
|
||||||
|
Parts of the data can be omitted by setting `omit = true`. In this case,
|
||||||
|
you only need to specify the length of the chunk to omit by either using
|
||||||
|
the `type` or `bits` setting. All other options can be skipped.
|
||||||
|
|
||||||
|
### Filter definitions
|
||||||
|
|
||||||
|
Filters can be used to match the length or the content of the data against
|
||||||
|
a specified reference. See the [examples section](#examples) for details.
|
||||||
|
You can also check multiple parts of the message by specifying multiple
|
||||||
|
`section` entries for a filter. Each `section` is then matched separately.
|
||||||
|
All have to match to apply the configuration.
|
||||||
|
|
||||||
|
#### `length` and `length_min` options
|
||||||
|
|
||||||
|
Using the `length` option, the filter will check if the data to parse has
|
||||||
|
exactly the given number of _bytes_. Otherwise, the configuration will not
|
||||||
|
be applied.
|
||||||
|
Similarly, for `length_min` the data has to have _at least_ the given number
|
||||||
|
of _bytes_ to generate a match.
|
||||||
|
|
||||||
|
#### `selection` list
|
||||||
|
|
||||||
|
Selections can be used with or without length constraints to match the content
|
||||||
|
of the data. Here, the `offset` and `bits` properties will specify the start
|
||||||
|
and length of the data to check. Both values are in _bit_ allowing for non-byte
|
||||||
|
aligned value extraction. The extracted data will the be checked against the
|
||||||
|
given `match` value specified in HEX.
|
||||||
|
|
||||||
|
If multiple `selection` entries are specified _all_ of the selections must
|
||||||
|
match for the configuration to get applied.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
In the following example, we use a binary protocol with three different messages
|
||||||
|
in little-endian format
|
||||||
|
|
||||||
|
### Message A definition
|
||||||
|
|
||||||
|
```text
|
||||||
|
+--------+------+------+--------+--------+------------+--------------------+--------------------+
|
||||||
|
| ID | type | len | addr | count | failure | value | timestamp |
|
||||||
|
+--------+------+------+--------+--------+------------+--------------------+--------------------+
|
||||||
|
| 0x0201 | 0x0A | 0x18 | 0x7F01 | 0x2A00 | 0x00000000 | 0x6F1283C0CA210940 | 0x10D4DF6200000000 |
|
||||||
|
+--------+------+------+--------+--------+------------+--------------------+--------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message B definition
|
||||||
|
|
||||||
|
```text
|
||||||
|
+--------+------+------+------------+
|
||||||
|
| ID | type | len | value |
|
||||||
|
+--------+------+------+------------+
|
||||||
|
| 0x0201 | 0x0B | 0x04 | 0xDEADC0DE |
|
||||||
|
+--------+------+------+------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message C definition
|
||||||
|
|
||||||
|
```text
|
||||||
|
+--------+------+------+------------+------------+--------------------+
|
||||||
|
| ID | type | len | value x | value y | timestamp |
|
||||||
|
+--------+------+------+------------+------------+--------------------+
|
||||||
|
| 0x0201 | 0x0C | 0x10 | 0x4DF82D40 | 0x5F305C08 | 0x10D4DF6200000000 |
|
||||||
|
+--------+------+------+------------+------------+--------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
All messages consists of a 4-byte header containing the _message type_
|
||||||
|
in the 3rd byte and a message specific body. To parse those messages
|
||||||
|
you can use the following configuration
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[inputs.file]]
|
||||||
|
files = ["messageA.bin", "messageB.bin", "messageC.bin"]
|
||||||
|
data_format = "binary"
|
||||||
|
endianess = "le"
|
||||||
|
|
||||||
|
[[inputs.file.binary]]
|
||||||
|
metric_name = "messageA"
|
||||||
|
|
||||||
|
entries = [
|
||||||
|
{ bits = 32, omit = true },
|
||||||
|
{ name = "address", type = "uint16", assignment = "tag" },
|
||||||
|
{ name = "count", type = "int16" },
|
||||||
|
{ name = "failure", type = "bool", bits = 32, assignment = "tag" },
|
||||||
|
{ name = "value", type = "float64" },
|
||||||
|
{ type = "unix", assignment = "time" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[inputs.file.binary.filter]
|
||||||
|
selection = [{ offset = 16, bits = 8, match = "0x0A" }]
|
||||||
|
|
||||||
|
[[inputs.file.binary]]
|
||||||
|
metric_name = "messageB"
|
||||||
|
|
||||||
|
entries = [
|
||||||
|
{ bits = 32, omit = true },
|
||||||
|
{ name = "value", type = "uint32" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[inputs.file.binary.filter]
|
||||||
|
selection = [{ offset = 16, bits = 8, match = "0x0B" }]
|
||||||
|
|
||||||
|
[[inputs.file.binary]]
|
||||||
|
metric_name = "messageC"
|
||||||
|
|
||||||
|
entries = [
|
||||||
|
{ bits = 32, omit = true },
|
||||||
|
{ name = "x", type = "float32" },
|
||||||
|
{ name = "y", type = "float32" },
|
||||||
|
{ type = "unix", assignment = "time" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[inputs.file.binary.filter]
|
||||||
|
selection = [{ offset = 16, bits = 8, match = "0x0C" }]
|
||||||
|
```
|
||||||
|
|
||||||
|
The above configuration has one `[[inputs.file.binary]]` section per
|
||||||
|
message type and uses a filter in each of those sections to apply
|
||||||
|
the correct configuration by comparing the 3rd byte (containing
|
||||||
|
the message type). This will lead to the following output
|
||||||
|
|
||||||
|
```text
|
||||||
|
> metricA,address=383,failure=false count=42i,value=3.1415 1658835984000000000
|
||||||
|
> metricB value=3737169374i 1658847037000000000
|
||||||
|
> metricC x=2.718280076980591,y=0.0000000000000000000000000000000006626070178575745 1658835984000000000
|
||||||
|
```
|
||||||
|
|
||||||
|
where `metricB` uses the parsing time as timestamp due to missing
|
||||||
|
information in the data. The other two metrics use the timestamp
|
||||||
|
derived from the data.
|
||||||
|
|
||||||
|
[time const]: https://golang.org/pkg/time/#pkg-constants
|
||||||
|
[time parse]: https://golang.org/pkg/time/#Parse
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
package binary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/internal"
|
||||||
|
"github.com/influxdata/telegraf/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BinaryPart struct {
|
||||||
|
Offset uint64 `toml:"offset"`
|
||||||
|
Bits uint64 `toml:"bits"`
|
||||||
|
Match string `toml:"match"`
|
||||||
|
|
||||||
|
val []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
Selection []BinaryPart `toml:"selection"`
|
||||||
|
LengthMin uint64 `toml:"length_min"`
|
||||||
|
Length uint64 `toml:"length"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
MetricName string `toml:"metric_name"`
|
||||||
|
Filter *Filter `toml:"filter"`
|
||||||
|
Entries []Entry `toml:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) preprocess(defaultName string) error {
|
||||||
|
// Preprocess filter part
|
||||||
|
if c.Filter != nil {
|
||||||
|
if c.Filter.Length != 0 && c.Filter.LengthMin != 0 {
|
||||||
|
return errors.New("length and length_min cannot be used together")
|
||||||
|
}
|
||||||
|
|
||||||
|
var length uint64
|
||||||
|
for i, s := range c.Filter.Selection {
|
||||||
|
end := (s.Offset + s.Bits) / 8
|
||||||
|
if (s.Offset+s.Bits)%8 != 0 {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
if end > length {
|
||||||
|
length = end
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
s.val, err = hex.DecodeString(strings.TrimPrefix(s.Match, "0x"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decoding match %d failed: %w", i, err)
|
||||||
|
}
|
||||||
|
c.Filter.Selection[i] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Filter.Length != 0 && length > c.Filter.Length {
|
||||||
|
return fmt.Errorf("filter length (%d) larger than constraint (%d)", length, c.Filter.Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Filter.Length == 0 && length > c.Filter.LengthMin {
|
||||||
|
c.Filter.LengthMin = length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preprocess entries part
|
||||||
|
var hasField, hasMeasurement bool
|
||||||
|
defined := make(map[string]bool)
|
||||||
|
for i, e := range c.Entries {
|
||||||
|
if err := e.check(); err != nil {
|
||||||
|
return fmt.Errorf("entry %q (%d): %w", e.Name, i, err)
|
||||||
|
}
|
||||||
|
// Store the normalized entry
|
||||||
|
c.Entries[i] = e
|
||||||
|
|
||||||
|
if e.Omit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate entries
|
||||||
|
key := e.Assignment + "_" + e.Name
|
||||||
|
if defined[key] {
|
||||||
|
return fmt.Errorf("multiple definitions of %q", e.Name)
|
||||||
|
}
|
||||||
|
defined[key] = true
|
||||||
|
hasMeasurement = hasMeasurement || e.Assignment == "measurement"
|
||||||
|
hasField = hasField || e.Assignment == "field"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasMeasurement && c.MetricName == "" {
|
||||||
|
if defaultName == "" {
|
||||||
|
return errors.New("no metric name given")
|
||||||
|
}
|
||||||
|
c.MetricName = defaultName
|
||||||
|
}
|
||||||
|
if !hasField {
|
||||||
|
return errors.New("no field defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) matches(in []byte) bool {
|
||||||
|
// If no filter is given, just match everything
|
||||||
|
if c.Filter == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking length constraints
|
||||||
|
length := uint64(len(in))
|
||||||
|
if c.Filter.Length != 0 && length != c.Filter.Length {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.Filter.LengthMin != 0 && length < c.Filter.LengthMin {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matching elements
|
||||||
|
for _, s := range c.Filter.Selection {
|
||||||
|
data, err := extractPart(in, s.Offset, s.Bits)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(data) != len(s.val) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, v := range data {
|
||||||
|
if v != s.val[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) collect(in []byte, order binary.ByteOrder, defaultTime time.Time) (telegraf.Metric, error) {
|
||||||
|
t := defaultTime
|
||||||
|
name := c.MetricName
|
||||||
|
tags := make(map[string]string)
|
||||||
|
fields := make(map[string]interface{})
|
||||||
|
|
||||||
|
var offset uint64
|
||||||
|
for _, e := range c.Entries {
|
||||||
|
data, n, err := e.extract(in, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset += n
|
||||||
|
|
||||||
|
switch e.Assignment {
|
||||||
|
case "measurement":
|
||||||
|
name = convertStringType(data)
|
||||||
|
case "field":
|
||||||
|
v, err := e.convertType(data, order)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("field %q failed: %w", e.Name, err)
|
||||||
|
}
|
||||||
|
fields[e.Name] = v
|
||||||
|
case "tag":
|
||||||
|
raw, err := e.convertType(data, order)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("tag %q failed: %w", e.Name, err)
|
||||||
|
}
|
||||||
|
v, err := internal.ToString(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("tag %q failed: %w", e.Name, err)
|
||||||
|
}
|
||||||
|
tags[e.Name] = v
|
||||||
|
case "time":
|
||||||
|
var err error
|
||||||
|
t, err = e.convertTimeType(data, order)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("time failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metric.New(name, tags, fields, t), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
package binary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
Type string `toml:"type"`
|
||||||
|
Bits uint64 `toml:"bits"`
|
||||||
|
Omit bool `toml:"omit"`
|
||||||
|
Terminator string `toml:"terminator"`
|
||||||
|
Timezone string `toml:"timezone"`
|
||||||
|
Assignment string `toml:"assignment"`
|
||||||
|
|
||||||
|
termination []byte
|
||||||
|
location *time.Location
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) check() error {
|
||||||
|
// Normalize cases
|
||||||
|
e.Assignment = strings.ToLower(e.Assignment)
|
||||||
|
e.Terminator = strings.ToLower(e.Terminator)
|
||||||
|
if e.Assignment != "time" {
|
||||||
|
e.Type = strings.ToLower(e.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle omitted fields
|
||||||
|
if e.Omit {
|
||||||
|
if e.Bits == 0 && e.Type == "" {
|
||||||
|
return errors.New("neither type nor bits given")
|
||||||
|
}
|
||||||
|
if e.Bits == 0 {
|
||||||
|
bits, err := bitsForType(e.Type)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.Bits = bits
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set name for global options
|
||||||
|
if e.Assignment == "measurement" || e.Assignment == "time" {
|
||||||
|
e.Name = e.Assignment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the name
|
||||||
|
if e.Name == "" {
|
||||||
|
return errors.New("missing name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the assignment
|
||||||
|
var defaultType string
|
||||||
|
switch e.Assignment {
|
||||||
|
case "measurement":
|
||||||
|
defaultType = "string"
|
||||||
|
if e.Type != "string" && e.Type != "" {
|
||||||
|
return errors.New("'measurement' type has to be 'string'")
|
||||||
|
}
|
||||||
|
case "time":
|
||||||
|
bits := uint64(64)
|
||||||
|
|
||||||
|
switch e.Type {
|
||||||
|
// Make 'unix' the default
|
||||||
|
case "":
|
||||||
|
defaultType = "unix"
|
||||||
|
// Special plugin specific names
|
||||||
|
case "unix", "unix_ms", "unix_us", "unix_ns":
|
||||||
|
// Format-specification string formats
|
||||||
|
default:
|
||||||
|
bits = uint64(len(e.Type) * 8)
|
||||||
|
}
|
||||||
|
if e.Bits == 0 {
|
||||||
|
e.Bits = bits
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Timezone {
|
||||||
|
case "", "utc":
|
||||||
|
// Make UTC the default
|
||||||
|
e.location = time.UTC
|
||||||
|
case "local":
|
||||||
|
e.location = time.Local
|
||||||
|
default:
|
||||||
|
var err error
|
||||||
|
e.location, err = time.LoadLocation(e.Timezone)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "tag":
|
||||||
|
defaultType = "string"
|
||||||
|
case "", "field":
|
||||||
|
e.Assignment = "field"
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("no assignment for %q", e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check type (special type for "time")
|
||||||
|
switch e.Type {
|
||||||
|
case "uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64":
|
||||||
|
fallthrough
|
||||||
|
case "float32", "float64":
|
||||||
|
bits, err := bitsForType(e.Type)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e.Bits == 0 {
|
||||||
|
e.Bits = bits
|
||||||
|
}
|
||||||
|
if bits < e.Bits {
|
||||||
|
return fmt.Errorf("type overflow for %q", e.Name)
|
||||||
|
}
|
||||||
|
case "bool":
|
||||||
|
if e.Bits == 0 {
|
||||||
|
e.Bits = 1
|
||||||
|
}
|
||||||
|
case "string":
|
||||||
|
// Check termination
|
||||||
|
switch e.Terminator {
|
||||||
|
case "", "fixed":
|
||||||
|
e.Terminator = "fixed"
|
||||||
|
if e.Bits == 0 {
|
||||||
|
return fmt.Errorf("require 'bits' for fixed-length string for %q", e.Name)
|
||||||
|
}
|
||||||
|
case "null":
|
||||||
|
e.termination = []byte{0}
|
||||||
|
if e.Bits != 0 {
|
||||||
|
return fmt.Errorf("cannot use 'bits' and 'null' terminator together for %q", e.Name)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if e.Bits != 0 {
|
||||||
|
return fmt.Errorf("cannot use 'bits' and terminator together for %q", e.Name)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
e.termination, err = hex.DecodeString(strings.TrimPrefix(e.Terminator, "0x"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decoding terminator failed for %q: %w", e.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can only handle strings that adhere to byte-bounds
|
||||||
|
if e.Bits%8 != 0 {
|
||||||
|
return fmt.Errorf("non-byte length for string field %q", e.Name)
|
||||||
|
}
|
||||||
|
case "":
|
||||||
|
if defaultType == "" {
|
||||||
|
return fmt.Errorf("no type for %q", e.Name)
|
||||||
|
}
|
||||||
|
e.Type = defaultType
|
||||||
|
default:
|
||||||
|
if e.Assignment != "time" {
|
||||||
|
return fmt.Errorf("unknown type for %q", e.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) extract(in []byte, offset uint64) ([]byte, uint64, error) {
|
||||||
|
if e.Bits > 0 {
|
||||||
|
data, err := extractPart(in, offset, e.Bits)
|
||||||
|
return data, e.Bits, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type != "string" {
|
||||||
|
return nil, 0, fmt.Errorf("unexpected entry: %v", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
inbits := uint64(len(in)) * 8
|
||||||
|
|
||||||
|
// Read up to the termination
|
||||||
|
var found bool
|
||||||
|
var data []byte
|
||||||
|
var termOffset int
|
||||||
|
var n uint64
|
||||||
|
for offset+n+8 <= inbits {
|
||||||
|
buf, err := extractPart(in, offset+n, 8)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(buf) != 1 {
|
||||||
|
return nil, 0, fmt.Errorf("unexpected length %d", len(buf))
|
||||||
|
}
|
||||||
|
data = append(data, buf[0])
|
||||||
|
n += 8
|
||||||
|
|
||||||
|
// Check for terminator
|
||||||
|
if buf[0] == e.termination[termOffset] {
|
||||||
|
termOffset++
|
||||||
|
}
|
||||||
|
if termOffset == len(e.termination) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, n, fmt.Errorf("terminator not found for %q", e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the terminator
|
||||||
|
return data[:len(data)-len(e.termination)], n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) convertType(in []byte, order binary.ByteOrder) (interface{}, error) {
|
||||||
|
switch e.Type {
|
||||||
|
case "uint8", "int8", "uint16", "int16", "uint32", "int32", "float32", "uint64", "int64", "float64":
|
||||||
|
return convertNumericType(in, e.Type, order)
|
||||||
|
case "bool":
|
||||||
|
return convertBoolType(in), nil
|
||||||
|
case "string":
|
||||||
|
return convertStringType(in), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("cannot handle type %q", e.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) convertTimeType(in []byte, order binary.ByteOrder) (time.Time, error) {
|
||||||
|
factor := int64(1)
|
||||||
|
|
||||||
|
switch e.Type {
|
||||||
|
case "unix":
|
||||||
|
factor *= 1000
|
||||||
|
fallthrough
|
||||||
|
case "unix_ms":
|
||||||
|
factor *= 1000
|
||||||
|
fallthrough
|
||||||
|
case "unix_us":
|
||||||
|
factor *= 1000
|
||||||
|
fallthrough
|
||||||
|
case "unix_ns":
|
||||||
|
raw, err := convertNumericType(in, "int64", order)
|
||||||
|
if err != nil {
|
||||||
|
return time.Unix(0, 0), err
|
||||||
|
}
|
||||||
|
v := raw.(int64)
|
||||||
|
return time.Unix(0, v*factor).In(e.location), nil
|
||||||
|
}
|
||||||
|
// We have a format specification (hopefully)
|
||||||
|
v := convertStringType(in)
|
||||||
|
return time.ParseInLocation(e.Type, v, e.location)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertStringType(in []byte) string {
|
||||||
|
return string(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertNumericType(in []byte, t string, order binary.ByteOrder) (interface{}, error) {
|
||||||
|
bits, err := bitsForType(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inlen := uint64(len(in))
|
||||||
|
expected := bits / 8
|
||||||
|
if inlen > expected {
|
||||||
|
// Should never happen
|
||||||
|
return 0, fmt.Errorf("too many bytes %d vs %d", len(in), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad the data if shorter than the datatype length
|
||||||
|
buf := make([]byte, expected-inlen, expected)
|
||||||
|
buf = append(buf, in...)
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case "uint8":
|
||||||
|
return buf[0], nil
|
||||||
|
case "int8":
|
||||||
|
return int8(buf[0]), nil
|
||||||
|
case "uint16":
|
||||||
|
return order.Uint16(buf), nil
|
||||||
|
case "int16":
|
||||||
|
v := order.Uint16(buf)
|
||||||
|
return int16(v), nil
|
||||||
|
case "uint32":
|
||||||
|
return order.Uint32(buf), nil
|
||||||
|
case "int32":
|
||||||
|
v := order.Uint32(buf)
|
||||||
|
return int32(v), nil
|
||||||
|
case "uint64":
|
||||||
|
return order.Uint64(buf), nil
|
||||||
|
case "int64":
|
||||||
|
v := order.Uint64(buf)
|
||||||
|
return int64(v), nil
|
||||||
|
case "float32":
|
||||||
|
v := order.Uint32(buf)
|
||||||
|
return math.Float32frombits(v), nil
|
||||||
|
case "float64":
|
||||||
|
v := order.Uint64(buf)
|
||||||
|
return math.Float64frombits(v), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no numeric type %q", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertBoolType(in []byte) bool {
|
||||||
|
for _, x := range in {
|
||||||
|
if x != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package binary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEntryExtract(t *testing.T) {
|
||||||
|
testdata := []byte{0x01, 0x02, 0x03, 0x04}
|
||||||
|
|
||||||
|
e := &Entry{Type: "uint64"}
|
||||||
|
_, _, err := e.extract(testdata, 0)
|
||||||
|
require.EqualError(t, err, `unexpected entry: &{ uint64 0 false [] <nil>}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryConvertType(t *testing.T) {
|
||||||
|
testdata := []byte{0x01, 0x02, 0x03, 0x04}
|
||||||
|
|
||||||
|
e := &Entry{Type: "garbage"}
|
||||||
|
_, err := e.convertType(testdata, hostEndianess)
|
||||||
|
require.EqualError(t, err, `cannot handle type "garbage"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryConvertTimeType(t *testing.T) {
|
||||||
|
testdata := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}
|
||||||
|
|
||||||
|
e := &Entry{Type: "unix_ns", location: time.UTC}
|
||||||
|
_, err := e.convertTimeType(testdata, hostEndianess)
|
||||||
|
require.EqualError(t, err, `too many bytes 9 vs 8`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertNumericType(t *testing.T) {
|
||||||
|
testdata := []byte{0x01, 0x02, 0x03, 0x04}
|
||||||
|
|
||||||
|
_, err := convertNumericType(testdata, "garbage", hostEndianess)
|
||||||
|
require.EqualError(t, err, `cannot determine length for type "garbage"`)
|
||||||
|
|
||||||
|
_, err = convertNumericType(testdata, "uint8", hostEndianess)
|
||||||
|
require.EqualError(t, err, `too many bytes 4 vs 1`)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
//go:build arm64be
|
||||||
|
// +build arm64be
|
||||||
|
|
||||||
|
package binary
|
||||||
|
|
||||||
|
import "encoding/binary"
|
||||||
|
|
||||||
|
var hostEndianess = binary.BigEndian
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
//go:build !arm64be
|
||||||
|
// +build !arm64be
|
||||||
|
|
||||||
|
package binary
|
||||||
|
|
||||||
|
import "encoding/binary"
|
||||||
|
|
||||||
|
var hostEndianess = binary.LittleEndian
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
package binary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/plugins/parsers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
AllowNoMatch bool `toml:"allow_no_match"`
|
||||||
|
Endianess string `toml:"endianess"`
|
||||||
|
Configs []Config `toml:"binary"`
|
||||||
|
Log telegraf.Logger `toml:"-"`
|
||||||
|
|
||||||
|
metricName string
|
||||||
|
defaultTags map[string]string
|
||||||
|
converter binary.ByteOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Init() error {
|
||||||
|
switch p.Endianess {
|
||||||
|
case "le":
|
||||||
|
p.converter = binary.LittleEndian
|
||||||
|
case "be":
|
||||||
|
p.converter = binary.BigEndian
|
||||||
|
case "", "host":
|
||||||
|
p.converter = hostEndianess
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown endianess %q", p.Endianess)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-process the configurations
|
||||||
|
if len(p.Configs) == 0 {
|
||||||
|
return errors.New("no configuration given")
|
||||||
|
}
|
||||||
|
for i, cfg := range p.Configs {
|
||||||
|
if err := cfg.preprocess(p.metricName); err != nil {
|
||||||
|
return fmt.Errorf("config %d invalid: %w", i, err)
|
||||||
|
}
|
||||||
|
p.Configs[i] = cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
|
||||||
|
t := time.Now()
|
||||||
|
|
||||||
|
matches := 0
|
||||||
|
metrics := make([]telegraf.Metric, 0)
|
||||||
|
for i, cfg := range p.Configs {
|
||||||
|
// Apply the filter and see if we should match this
|
||||||
|
if !cfg.matches(buf) {
|
||||||
|
p.Log.Debugf("ignoring data in config %d", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matches++
|
||||||
|
|
||||||
|
// Collect the metric
|
||||||
|
m, err := cfg.collect(buf, p.converter, t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
metrics = append(metrics, m)
|
||||||
|
}
|
||||||
|
if matches == 0 && !p.AllowNoMatch {
|
||||||
|
return nil, errors.New("no matching configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) ParseLine(line string) (telegraf.Metric, error) {
|
||||||
|
metrics, err := p.Parse([]byte(line))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(metrics) {
|
||||||
|
case 0:
|
||||||
|
return nil, nil
|
||||||
|
case 1:
|
||||||
|
return metrics[0], nil
|
||||||
|
default:
|
||||||
|
return metrics[0], fmt.Errorf("cannot parse line with multiple (%d) metrics", len(metrics))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) SetDefaultTags(tags map[string]string) {
|
||||||
|
p.defaultTags = tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Register all variants
|
||||||
|
parsers.Add("binary",
|
||||||
|
func(defaultMetricName string) telegraf.Parser {
|
||||||
|
return &Parser{metricName: defaultMetricName}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPart(in []byte, offset, bits uint64) ([]byte, error) {
|
||||||
|
inLen := uint64(len(in))
|
||||||
|
|
||||||
|
start := offset / 8
|
||||||
|
bitend := offset%8 + bits
|
||||||
|
length := bitend / 8
|
||||||
|
if bitend%8 != 0 {
|
||||||
|
length++
|
||||||
|
}
|
||||||
|
|
||||||
|
if start+length > inLen {
|
||||||
|
return nil, fmt.Errorf("out-of-bounds @%d with %d bits", offset, bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []byte
|
||||||
|
out = append(out, in[start:start+length]...)
|
||||||
|
|
||||||
|
if offset%8 != 0 {
|
||||||
|
// Mask the start-byte with the non-aligned bit-mask
|
||||||
|
startmask := (byte(1) << (8 - offset%8)) - 1
|
||||||
|
out[0] = out[0] & startmask
|
||||||
|
}
|
||||||
|
|
||||||
|
if bitend%8 == 0 {
|
||||||
|
// The end is aligned to byte-boundaries
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
shift := 8 - bitend%8
|
||||||
|
carryshift := bitend % 8
|
||||||
|
|
||||||
|
// We need to shift right in case of not ending at a byte boundary
|
||||||
|
// to make the bits right aligned.
|
||||||
|
// Carry over the bits from the byte left to fill in...
|
||||||
|
var carry byte
|
||||||
|
for i, x := range out {
|
||||||
|
out[i] = (x >> shift) | carry
|
||||||
|
carry = x << carryshift
|
||||||
|
}
|
||||||
|
|
||||||
|
if bits%8 == 0 {
|
||||||
|
// Avoid an empty leading byte
|
||||||
|
return out[1:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bitsForType(t string) (uint64, error) {
|
||||||
|
switch t {
|
||||||
|
case "uint8", "int8":
|
||||||
|
return 8, nil
|
||||||
|
case "uint16", "int16":
|
||||||
|
return 16, nil
|
||||||
|
case "uint32", "int32", "float32":
|
||||||
|
return 32, nil
|
||||||
|
case "uint64", "int64", "float64":
|
||||||
|
return 64, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("cannot determine length for type %q", t)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,3 @@
|
||||||
|
metricA,address=383,failure=false count=42i,value=3.1415 1658835984000000000
|
||||||
|
metricB value=3737169374u 1658835984000000000
|
||||||
|
metricC x=2.718280076980591,y=0.0000000000000000000000000000000006626070178575745 1658835984000000000
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,46 @@
|
||||||
|
[[inputs.file]]
|
||||||
|
files = ["./testcases/multiple_messages/messageA.bin", "./testcases/multiple_messages/messageB.bin", "./testcases/multiple_messages/messageC.bin"]
|
||||||
|
data_format = "binary"
|
||||||
|
endianess = "le"
|
||||||
|
|
||||||
|
[[inputs.file.binary]]
|
||||||
|
metric_name = "metricA"
|
||||||
|
|
||||||
|
entries = [
|
||||||
|
{ bits = 32, omit = true },
|
||||||
|
{ name = "address", type = "uint16", assignment = "tag" },
|
||||||
|
{ name = "count", type = "int16" },
|
||||||
|
{ name = "failure", type = "bool", bits = 32, assignment = "tag" },
|
||||||
|
{ name = "value", type = "float64" },
|
||||||
|
{ type = "unix", assignment = "time" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[inputs.file.binary.filter]
|
||||||
|
selection = [
|
||||||
|
{ offset = 16, bits = 8, match = "0x0A" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[inputs.file.binary]]
|
||||||
|
metric_name = "metricB"
|
||||||
|
|
||||||
|
entries = [
|
||||||
|
{ bits = 32, omit = true },
|
||||||
|
{ name = "value", type = "uint32" },
|
||||||
|
{ type = "unix", assignment = "time" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[inputs.file.binary.filter]
|
||||||
|
selection = [{ offset = 16, bits = 8, match = "0x0B" }]
|
||||||
|
|
||||||
|
[[inputs.file.binary]]
|
||||||
|
metric_name = "metricC"
|
||||||
|
|
||||||
|
entries = [
|
||||||
|
{ bits = 32, omit = true },
|
||||||
|
{ name = "x", type = "float32" },
|
||||||
|
{ name = "y", type = "float32" },
|
||||||
|
{ type = "unix", assignment = "time" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[inputs.file.binary.filter]
|
||||||
|
selection = [{ offset = 16, bits = 8, match = "0x0C" }]
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/influxdata/telegraf"
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/internal/choice"
|
||||||
"github.com/influxdata/telegraf/plugins/parsers"
|
"github.com/influxdata/telegraf/plugins/parsers"
|
||||||
_ "github.com/influxdata/telegraf/plugins/parsers/all"
|
_ "github.com/influxdata/telegraf/plugins/parsers/all"
|
||||||
)
|
)
|
||||||
|
|
@ -43,7 +44,14 @@ func TestRegistry_BackwardCompatibility(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define parsers that do not have an old-school init
|
||||||
|
newStyleOnly := []string{"binary"}
|
||||||
|
|
||||||
for name, creator := range parsers.Parsers {
|
for name, creator := range parsers.Parsers {
|
||||||
|
if choice.Contains(name, newStyleOnly) {
|
||||||
|
t.Logf("skipping new-style-only %q...", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
t.Logf("testing %q...", name)
|
t.Logf("testing %q...", name)
|
||||||
cfg.DataFormat = name
|
cfg.DataFormat = name
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue