Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
350
plugins/parsers/binary/README.md
Normal file
350
plugins/parsers/binary/README.md
Normal file
|
@ -0,0 +1,350 @@
|
|||
# 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 endianness of the data.
|
||||
## Available values are "be" (big-endian), "le" (little-endian) and "host",
|
||||
## where "host" means the same endianness as the machine running Telegraf.
|
||||
# endianness = "host"
|
||||
|
||||
## Interpret input using the specified encoding
|
||||
## Available values are "none" (raw bytes), "hex" and "base64"
|
||||
# binary_encoding = "none"
|
||||
|
||||
## 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 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.
|
||||
|
||||
#### `endianness` (optional)
|
||||
|
||||
This specifies the endianness of the data. If not specified, the parser will
|
||||
fallback to the "host" endianness, assuming that the message and Telegraf
|
||||
machine share the same endianness.
|
||||
Alternatively, you can explicitly specify big-endian format (`"be"`) or
|
||||
little-endian format (`"le"`).
|
||||
|
||||
#### `binary_encoding` (optional)
|
||||
|
||||
If this option is not specified or set to `none`, the input data contains the
|
||||
binary data as raw bytes. This is the default.
|
||||
|
||||
If set to `hex`, the input data is interpreted as a string containing
|
||||
hex-encoded data like `C0 C7 21 A9`. The value is _case insensitive_ and can
|
||||
handle spaces and prefixes like `0x` or `x`.
|
||||
|
||||
If set to `base64` the input data is interpreted as a string containing
|
||||
padded base64 data `RDLAAA==`.
|
||||
|
||||
### 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"
|
||||
endianness = "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
|
183
plugins/parsers/binary/config.go
Normal file
183
plugins/parsers/binary/config.go
Normal file
|
@ -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
|
||||
}
|
310
plugins/parsers/binary/entry.go
Normal file
310
plugins/parsers/binary/entry.go
Normal file
|
@ -0,0 +1,310 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
)
|
||||
|
||||
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 internal.ParseTimestamp(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
|
||||
}
|
44
plugins/parsers/binary/entry_test.go
Normal file
44
plugins/parsers/binary/entry_test.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
)
|
||||
|
||||
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, internal.HostEndianness)
|
||||
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, internal.HostEndianness)
|
||||
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", internal.HostEndianness)
|
||||
require.EqualError(t, err, `cannot determine length for type "garbage"`)
|
||||
|
||||
_, err = convertNumericType(testdata, "uint8", internal.HostEndianness)
|
||||
require.EqualError(t, err, `too many bytes 4 vs 1`)
|
||||
}
|
213
plugins/parsers/binary/parser.go
Normal file
213
plugins/parsers/binary/parser.go
Normal file
|
@ -0,0 +1,213 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/plugins/parsers"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
AllowNoMatch bool `toml:"allow_no_match"`
|
||||
Endianess string `toml:"endianess" deprecated:"1.27.4;1.35.0;use 'endianness' instead"`
|
||||
Endianness string `toml:"endianness"`
|
||||
Configs []Config `toml:"binary"`
|
||||
HexEncoding bool `toml:"hex_encoding" deprecated:"1.30.0;1.35.0;use 'binary_encoding' instead"`
|
||||
Encoding string `toml:"binary_encoding"`
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
|
||||
metricName string
|
||||
defaultTags map[string]string
|
||||
converter binary.ByteOrder
|
||||
}
|
||||
|
||||
func (p *Parser) Init() error {
|
||||
// Keep backward compatibility
|
||||
if p.Endianess != "" && p.Endianness == "" {
|
||||
p.Endianness = p.Endianess
|
||||
}
|
||||
if p.HexEncoding {
|
||||
if p.Encoding != "" && p.Encoding != "hex" {
|
||||
return errors.New("conflicting settings between 'hex_encoding' and 'binary_encoding'")
|
||||
}
|
||||
p.Encoding = "hex"
|
||||
}
|
||||
|
||||
switch p.Endianness {
|
||||
case "le":
|
||||
p.converter = binary.LittleEndian
|
||||
case "be":
|
||||
p.converter = binary.BigEndian
|
||||
case "", "host":
|
||||
p.converter = internal.HostEndianness
|
||||
default:
|
||||
return fmt.Errorf("unknown endianness %q", p.Endianness)
|
||||
}
|
||||
|
||||
switch p.Encoding {
|
||||
case "", "none", "hex", "base64":
|
||||
default:
|
||||
return fmt.Errorf("unknown encoding %q", p.Encoding)
|
||||
}
|
||||
|
||||
// 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(data []byte) ([]telegraf.Metric, error) {
|
||||
t := time.Now()
|
||||
|
||||
// If the data is encoded in HEX, we need to decode it first
|
||||
buf := data
|
||||
switch p.Encoding {
|
||||
case "hex":
|
||||
s := strings.TrimPrefix(string(data), "0x")
|
||||
s = strings.TrimPrefix(s, "x")
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.ReplaceAll(s, " ", "")
|
||||
s = strings.ReplaceAll(s, "\t", "")
|
||||
var err error
|
||||
buf, err = hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding hex failed: %w", err)
|
||||
}
|
||||
case "base64":
|
||||
decoder := base64.StdEncoding.WithPadding(base64.StdPadding)
|
||||
var err error
|
||||
buf, err = decoder.DecodeString(strings.TrimSpace(string(data)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding base64 failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
1614
plugins/parsers/binary/parser_test.go
Normal file
1614
plugins/parsers/binary/parser_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,3 @@
|
|||
test value=715
|
||||
test value=208.5
|
||||
test value=0.471
|
|
@ -0,0 +1 @@
|
|||
RDLAAA==
|
|
@ -0,0 +1 @@
|
|||
Q1CAAA==
|
|
@ -0,0 +1 @@
|
|||
PvEm6Q==
|
|
@ -0,0 +1,8 @@
|
|||
[[inputs.test]]
|
||||
files = ["messageA.bin", "messageB.bin", "messageC.bin"]
|
||||
data_format = "binary"
|
||||
endianness = "be"
|
||||
binary_encoding = "base64"
|
||||
|
||||
[[inputs.test.binary]]
|
||||
entries = [{ name = "value", type = "float32" }]
|
|
@ -0,0 +1,3 @@
|
|||
test value=715
|
||||
test value=208.5
|
||||
test value=0.471
|
|
@ -0,0 +1 @@
|
|||
0x4432c000
|
|
@ -0,0 +1 @@
|
|||
0x43508000
|
|
@ -0,0 +1 @@
|
|||
0x3ef126e9
|
|
@ -0,0 +1,8 @@
|
|||
[[inputs.test]]
|
||||
files = ["messageA.bin", "messageB.bin", "messageC.bin"]
|
||||
data_format = "binary"
|
||||
endianness = "be"
|
||||
binary_encoding = "hex"
|
||||
|
||||
[[inputs.test.binary]]
|
||||
entries = [{ name = "value", type = "float32" }]
|
|
@ -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
|
BIN
plugins/parsers/binary/testcases/multiple_messages/messageA.bin
Normal file
BIN
plugins/parsers/binary/testcases/multiple_messages/messageA.bin
Normal file
Binary file not shown.
BIN
plugins/parsers/binary/testcases/multiple_messages/messageB.bin
Normal file
BIN
plugins/parsers/binary/testcases/multiple_messages/messageB.bin
Normal file
Binary file not shown.
BIN
plugins/parsers/binary/testcases/multiple_messages/messageC.bin
Normal file
BIN
plugins/parsers/binary/testcases/multiple_messages/messageC.bin
Normal file
Binary file not shown.
|
@ -0,0 +1,46 @@
|
|||
[[inputs.test]]
|
||||
files = ["messageA.bin", "messageB.bin", "messageC.bin"]
|
||||
data_format = "binary"
|
||||
endianness = "le"
|
||||
|
||||
[[inputs.test.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.test.binary.filter]
|
||||
selection = [
|
||||
{ offset = 16, bits = 8, match = "0x0A" },
|
||||
]
|
||||
|
||||
[[inputs.test.binary]]
|
||||
metric_name = "metricB"
|
||||
|
||||
entries = [
|
||||
{ bits = 32, omit = true },
|
||||
{ name = "value", type = "uint32" },
|
||||
{ type = "unix", assignment = "time" },
|
||||
]
|
||||
|
||||
[inputs.test.binary.filter]
|
||||
selection = [{ offset = 16, bits = 8, match = "0x0B" }]
|
||||
|
||||
[[inputs.test.binary]]
|
||||
metric_name = "metricC"
|
||||
|
||||
entries = [
|
||||
{ bits = 32, omit = true },
|
||||
{ name = "x", type = "float32" },
|
||||
{ name = "y", type = "float32" },
|
||||
{ type = "unix", assignment = "time" },
|
||||
]
|
||||
|
||||
[inputs.test.binary.filter]
|
||||
selection = [{ offset = 16, bits = 8, match = "0x0C" }]
|
Loading…
Add table
Add a link
Reference in a new issue