fix: directory monitor input plugin when data format is CSV and csv_skip_rows>0 and csv_header_row_count>=1 (#9865)
This commit is contained in:
parent
b9e4978b17
commit
db86904759
|
|
@ -261,15 +261,12 @@ func (monitor *DirectoryMonitor) ingestFile(filePath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (monitor *DirectoryMonitor) parseFile(parser parsers.Parser, reader io.Reader, fileName string) error {
|
func (monitor *DirectoryMonitor) parseFile(parser parsers.Parser, reader io.Reader, fileName string) error {
|
||||||
// Read the file line-by-line and parse with the configured parse method.
|
|
||||||
firstLine := true
|
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
metrics, err := monitor.parseLine(parser, scanner.Bytes(), firstLine)
|
metrics, err := monitor.parseLine(parser, scanner.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
firstLine = false
|
|
||||||
|
|
||||||
if monitor.FileTag != "" {
|
if monitor.FileTag != "" {
|
||||||
for _, m := range metrics {
|
for _, m := range metrics {
|
||||||
|
|
@ -285,24 +282,17 @@ func (monitor *DirectoryMonitor) parseFile(parser parsers.Parser, reader io.Read
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (monitor *DirectoryMonitor) parseLine(parser parsers.Parser, line []byte, firstLine bool) ([]telegraf.Metric, error) {
|
func (monitor *DirectoryMonitor) parseLine(parser parsers.Parser, line []byte) ([]telegraf.Metric, error) {
|
||||||
switch parser.(type) {
|
switch parser.(type) {
|
||||||
case *csv.Parser:
|
case *csv.Parser:
|
||||||
// The CSV parser parses headers in Parse and skips them in ParseLine.
|
m, err := parser.Parse(line)
|
||||||
if firstLine {
|
|
||||||
return parser.Parse(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := parser.ParseLine(string(line))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return m, err
|
||||||
if m != nil {
|
|
||||||
return []telegraf.Metric{m}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return []telegraf.Metric{}, nil
|
|
||||||
default:
|
default:
|
||||||
return parser.Parse(line)
|
return parser.Parse(line)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ package directory_monitor
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/influxdata/telegraf/plugins/parsers"
|
"github.com/influxdata/telegraf/plugins/parsers"
|
||||||
"github.com/influxdata/telegraf/testutil"
|
"github.com/influxdata/telegraf/testutil"
|
||||||
)
|
)
|
||||||
|
|
@ -193,3 +192,224 @@ func TestFileTag(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCSVNoSkipRows(t *testing.T) {
|
||||||
|
acc := testutil.Accumulator{}
|
||||||
|
testCsvFile := "test.csv"
|
||||||
|
|
||||||
|
// Establish process directory and finished directory.
|
||||||
|
finishedDirectory, err := os.MkdirTemp("", "finished")
|
||||||
|
require.NoError(t, err)
|
||||||
|
processDirectory, err := os.MkdirTemp("", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(processDirectory)
|
||||||
|
defer os.RemoveAll(finishedDirectory)
|
||||||
|
|
||||||
|
// Init plugin.
|
||||||
|
r := DirectoryMonitor{
|
||||||
|
Directory: processDirectory,
|
||||||
|
FinishedDirectory: finishedDirectory,
|
||||||
|
MaxBufferedMetrics: 1000,
|
||||||
|
FileQueueSize: 100000,
|
||||||
|
}
|
||||||
|
err = r.Init()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
parserConfig := parsers.Config{
|
||||||
|
DataFormat: "csv",
|
||||||
|
CSVHeaderRowCount: 1,
|
||||||
|
CSVSkipRows: 0,
|
||||||
|
CSVTagColumns: []string{"line1"},
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
r.SetParserFunc(func() (parsers.Parser, error) {
|
||||||
|
return parsers.NewParser(&parserConfig)
|
||||||
|
})
|
||||||
|
r.Log = testutil.Logger{}
|
||||||
|
|
||||||
|
testCSV := `line1,line2,line3
|
||||||
|
hello,80,test_name2`
|
||||||
|
|
||||||
|
expectedFields := map[string]interface{}{
|
||||||
|
"line2": int64(80),
|
||||||
|
"line3": "test_name2",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write csv file to process into the 'process' directory.
|
||||||
|
f, err := os.Create(filepath.Join(processDirectory, testCsvFile))
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = f.WriteString(testCSV)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = f.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Start plugin before adding file.
|
||||||
|
err = r.Start(&acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = r.Gather(&acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
acc.Wait(1)
|
||||||
|
r.Stop()
|
||||||
|
|
||||||
|
// Verify that we read both files once.
|
||||||
|
require.Equal(t, len(acc.Metrics), 1)
|
||||||
|
|
||||||
|
// File should have gone back to the test directory, as we configured.
|
||||||
|
_, err = os.Stat(filepath.Join(finishedDirectory, testCsvFile))
|
||||||
|
require.NoError(t, err)
|
||||||
|
for _, m := range acc.Metrics {
|
||||||
|
for key, value := range m.Tags {
|
||||||
|
require.Equal(t, "line1", key)
|
||||||
|
require.Equal(t, "hello", value)
|
||||||
|
}
|
||||||
|
require.Equal(t, expectedFields, m.Fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSVSkipRows(t *testing.T) {
|
||||||
|
acc := testutil.Accumulator{}
|
||||||
|
testCsvFile := "test.csv"
|
||||||
|
|
||||||
|
// Establish process directory and finished directory.
|
||||||
|
finishedDirectory, err := os.MkdirTemp("", "finished")
|
||||||
|
require.NoError(t, err)
|
||||||
|
processDirectory, err := os.MkdirTemp("", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(processDirectory)
|
||||||
|
defer os.RemoveAll(finishedDirectory)
|
||||||
|
|
||||||
|
// Init plugin.
|
||||||
|
r := DirectoryMonitor{
|
||||||
|
Directory: processDirectory,
|
||||||
|
FinishedDirectory: finishedDirectory,
|
||||||
|
MaxBufferedMetrics: 1000,
|
||||||
|
FileQueueSize: 100000,
|
||||||
|
}
|
||||||
|
err = r.Init()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
parserConfig := parsers.Config{
|
||||||
|
DataFormat: "csv",
|
||||||
|
CSVHeaderRowCount: 1,
|
||||||
|
CSVSkipRows: 2,
|
||||||
|
CSVTagColumns: []string{"line1"},
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
r.SetParserFunc(func() (parsers.Parser, error) {
|
||||||
|
return parsers.NewParser(&parserConfig)
|
||||||
|
})
|
||||||
|
r.Log = testutil.Logger{}
|
||||||
|
|
||||||
|
testCSV := `garbage nonsense 1
|
||||||
|
garbage,nonsense,2
|
||||||
|
line1,line2,line3
|
||||||
|
hello,80,test_name2`
|
||||||
|
|
||||||
|
expectedFields := map[string]interface{}{
|
||||||
|
"line2": int64(80),
|
||||||
|
"line3": "test_name2",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write csv file to process into the 'process' directory.
|
||||||
|
f, err := os.Create(filepath.Join(processDirectory, testCsvFile))
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = f.WriteString(testCSV)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = f.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Start plugin before adding file.
|
||||||
|
err = r.Start(&acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = r.Gather(&acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
acc.Wait(1)
|
||||||
|
r.Stop()
|
||||||
|
|
||||||
|
// Verify that we read both files once.
|
||||||
|
require.Equal(t, len(acc.Metrics), 1)
|
||||||
|
|
||||||
|
// File should have gone back to the test directory, as we configured.
|
||||||
|
_, err = os.Stat(filepath.Join(finishedDirectory, testCsvFile))
|
||||||
|
require.NoError(t, err)
|
||||||
|
for _, m := range acc.Metrics {
|
||||||
|
for key, value := range m.Tags {
|
||||||
|
require.Equal(t, "line1", key)
|
||||||
|
require.Equal(t, "hello", value)
|
||||||
|
}
|
||||||
|
require.Equal(t, expectedFields, m.Fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSVMultiHeader(t *testing.T) {
|
||||||
|
acc := testutil.Accumulator{}
|
||||||
|
testCsvFile := "test.csv"
|
||||||
|
|
||||||
|
// Establish process directory and finished directory.
|
||||||
|
finishedDirectory, err := os.MkdirTemp("", "finished")
|
||||||
|
require.NoError(t, err)
|
||||||
|
processDirectory, err := os.MkdirTemp("", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(processDirectory)
|
||||||
|
defer os.RemoveAll(finishedDirectory)
|
||||||
|
|
||||||
|
// Init plugin.
|
||||||
|
r := DirectoryMonitor{
|
||||||
|
Directory: processDirectory,
|
||||||
|
FinishedDirectory: finishedDirectory,
|
||||||
|
MaxBufferedMetrics: 1000,
|
||||||
|
FileQueueSize: 100000,
|
||||||
|
}
|
||||||
|
err = r.Init()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
parserConfig := parsers.Config{
|
||||||
|
DataFormat: "csv",
|
||||||
|
CSVHeaderRowCount: 2,
|
||||||
|
CSVTagColumns: []string{"line1"},
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
r.SetParserFunc(func() (parsers.Parser, error) {
|
||||||
|
return parsers.NewParser(&parserConfig)
|
||||||
|
})
|
||||||
|
r.Log = testutil.Logger{}
|
||||||
|
|
||||||
|
testCSV := `line,line,line
|
||||||
|
1,2,3
|
||||||
|
hello,80,test_name2`
|
||||||
|
|
||||||
|
expectedFields := map[string]interface{}{
|
||||||
|
"line2": int64(80),
|
||||||
|
"line3": "test_name2",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write csv file to process into the 'process' directory.
|
||||||
|
f, err := os.Create(filepath.Join(processDirectory, testCsvFile))
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = f.WriteString(testCSV)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = f.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Start plugin before adding file.
|
||||||
|
err = r.Start(&acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = r.Gather(&acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
acc.Wait(1)
|
||||||
|
r.Stop()
|
||||||
|
|
||||||
|
// Verify that we read both files once.
|
||||||
|
require.Equal(t, len(acc.Metrics), 1)
|
||||||
|
|
||||||
|
// File should have gone back to the test directory, as we configured.
|
||||||
|
_, err = os.Stat(filepath.Join(finishedDirectory, testCsvFile))
|
||||||
|
require.NoError(t, err)
|
||||||
|
for _, m := range acc.Metrics {
|
||||||
|
for key, value := range m.Tags {
|
||||||
|
require.Equal(t, "line1", key)
|
||||||
|
require.Equal(t, "hello", value)
|
||||||
|
}
|
||||||
|
require.Equal(t, expectedFields, m.Fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,25 +288,17 @@ func (t *Tail) tailNewFiles(fromBeginning bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseLine parses a line of text.
|
// ParseLine parses a line of text.
|
||||||
func parseLine(parser parsers.Parser, line string, firstLine bool) ([]telegraf.Metric, error) {
|
func parseLine(parser parsers.Parser, line string) ([]telegraf.Metric, error) {
|
||||||
switch parser.(type) {
|
switch parser.(type) {
|
||||||
case *csv.Parser:
|
case *csv.Parser:
|
||||||
// The csv parser parses headers in Parse and skips them in ParseLine.
|
m, err := parser.Parse([]byte(line))
|
||||||
// As a temporary solution call Parse only when getting the first
|
|
||||||
// line from the file.
|
|
||||||
if firstLine {
|
|
||||||
return parser.Parse([]byte(line))
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := parser.ParseLine(line)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return m, err
|
||||||
if m != nil {
|
|
||||||
return []telegraf.Metric{m}, nil
|
|
||||||
}
|
|
||||||
return []telegraf.Metric{}, nil
|
|
||||||
default:
|
default:
|
||||||
return parser.Parse([]byte(line))
|
return parser.Parse([]byte(line))
|
||||||
}
|
}
|
||||||
|
|
@ -315,8 +307,6 @@ func parseLine(parser parsers.Parser, line string, firstLine bool) ([]telegraf.M
|
||||||
// Receiver is launched as a goroutine to continuously watch a tailed logfile
|
// Receiver is launched as a goroutine to continuously watch a tailed logfile
|
||||||
// for changes, parse any incoming msgs, and add to the accumulator.
|
// for changes, parse any incoming msgs, and add to the accumulator.
|
||||||
func (t *Tail) receiver(parser parsers.Parser, tailer *tail.Tail) {
|
func (t *Tail) receiver(parser parsers.Parser, tailer *tail.Tail) {
|
||||||
var firstLine = true
|
|
||||||
|
|
||||||
// holds the individual lines of multi-line log entries.
|
// holds the individual lines of multi-line log entries.
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
|
@ -378,13 +368,12 @@ func (t *Tail) receiver(parser parsers.Parser, tailer *tail.Tail) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics, err := parseLine(parser, text, firstLine)
|
metrics, err := parseLine(parser, text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log.Errorf("Malformed log line in %q: [%q]: %s",
|
t.Log.Errorf("Malformed log line in %q: [%q]: %s",
|
||||||
tailer.Filename, text, err.Error())
|
tailer.Filename, text, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
firstLine = false
|
|
||||||
|
|
||||||
if t.PathTag != "" {
|
if t.PathTag != "" {
|
||||||
for _, metric := range metrics {
|
for _, metric := range metrics {
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,67 @@ cpu,42
|
||||||
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics())
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCSVMultiHeaderWithSkipRowANDColumn(t *testing.T) {
|
||||||
|
tmpfile, err := os.CreateTemp("", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
_, err = tmpfile.WriteString(`garbage nonsense
|
||||||
|
skip,measurement,value
|
||||||
|
row,1,2
|
||||||
|
skip1,cpu,42
|
||||||
|
skip2,mem,100
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, tmpfile.Close())
|
||||||
|
|
||||||
|
plugin := NewTestTail()
|
||||||
|
plugin.Log = testutil.Logger{}
|
||||||
|
plugin.FromBeginning = true
|
||||||
|
plugin.Files = []string{tmpfile.Name()}
|
||||||
|
plugin.SetParserFunc(func() (parsers.Parser, error) {
|
||||||
|
return csv.NewParser(&csv.Config{
|
||||||
|
MeasurementColumn: "measurement1",
|
||||||
|
HeaderRowCount: 2,
|
||||||
|
SkipRows: 1,
|
||||||
|
SkipColumns: 1,
|
||||||
|
TimeFunc: func() time.Time { return time.Unix(0, 0) },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
err = plugin.Init()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
acc := testutil.Accumulator{}
|
||||||
|
err = plugin.Start(&acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer plugin.Stop()
|
||||||
|
err = plugin.Gather(&acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
acc.Wait(2)
|
||||||
|
plugin.Stop()
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric("cpu",
|
||||||
|
map[string]string{
|
||||||
|
"path": tmpfile.Name(),
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"value2": 42,
|
||||||
|
},
|
||||||
|
time.Unix(0, 0)),
|
||||||
|
testutil.MustMetric("mem",
|
||||||
|
map[string]string{
|
||||||
|
"path": tmpfile.Name(),
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"value2": 100,
|
||||||
|
},
|
||||||
|
time.Unix(0, 0)),
|
||||||
|
}
|
||||||
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics())
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure that the first line can produce multiple metrics (#6138)
|
// Ensure that the first line can produce multiple metrics (#6138)
|
||||||
func TestMultipleMetricsOnFirstLine(t *testing.T) {
|
func TestMultipleMetricsOnFirstLine(t *testing.T) {
|
||||||
tmpfile, err := os.CreateTemp("", "")
|
tmpfile, err := os.CreateTemp("", "")
|
||||||
|
|
|
||||||
|
|
@ -96,48 +96,68 @@ func (p *Parser) compile(r io.Reader) *csv.Reader {
|
||||||
|
|
||||||
func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
|
func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
|
||||||
r := bytes.NewReader(buf)
|
r := bytes.NewReader(buf)
|
||||||
|
return parseCSV(p, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLine does not use any information in header and assumes DataColumns is set
|
||||||
|
// it will also not skip any rows
|
||||||
|
func (p *Parser) ParseLine(line string) (telegraf.Metric, error) {
|
||||||
|
r := bytes.NewReader([]byte(line))
|
||||||
|
metrics, err := parseCSV(p, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(metrics) == 1 {
|
||||||
|
return metrics[0], nil
|
||||||
|
}
|
||||||
|
if len(metrics) > 1 {
|
||||||
|
return nil, fmt.Errorf("expected 1 metric found %d", len(metrics))
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCSV(p *Parser, r io.Reader) ([]telegraf.Metric, error) {
|
||||||
csvReader := p.compile(r)
|
csvReader := p.compile(r)
|
||||||
// skip first rows
|
// skip first rows
|
||||||
for i := 0; i < p.SkipRows; i++ {
|
for p.SkipRows > 0 {
|
||||||
_, err := csvReader.Read()
|
_, err := csvReader.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
p.SkipRows--
|
||||||
}
|
}
|
||||||
// if there is a header and we did not get DataColumns
|
// if there is a header, and we did not get DataColumns
|
||||||
// set DataColumns to names extracted from the header
|
// set DataColumns to names extracted from the header
|
||||||
// we always reread the header to avoid side effects
|
// we always reread the header to avoid side effects
|
||||||
// in cases where multiple files with different
|
// in cases where multiple files with different
|
||||||
// headers are read
|
// headers are read
|
||||||
|
for p.HeaderRowCount > 0 {
|
||||||
|
header, err := csvReader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.HeaderRowCount--
|
||||||
|
if p.gotColumnNames {
|
||||||
|
// Ignore header lines if columns are named
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
//concatenate header names
|
||||||
|
for i, h := range header {
|
||||||
|
name := h
|
||||||
|
if p.TrimSpace {
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
}
|
||||||
|
if len(p.ColumnNames) <= i {
|
||||||
|
p.ColumnNames = append(p.ColumnNames, name)
|
||||||
|
} else {
|
||||||
|
p.ColumnNames[i] = p.ColumnNames[i] + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if !p.gotColumnNames {
|
if !p.gotColumnNames {
|
||||||
headerNames := make([]string, 0)
|
// skip first rows
|
||||||
for i := 0; i < p.HeaderRowCount; i++ {
|
p.ColumnNames = p.ColumnNames[p.SkipColumns:]
|
||||||
header, err := csvReader.Read()
|
p.gotColumnNames = true
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
//concatenate header names
|
|
||||||
for i := range header {
|
|
||||||
name := header[i]
|
|
||||||
if p.TrimSpace {
|
|
||||||
name = strings.Trim(name, " ")
|
|
||||||
}
|
|
||||||
if len(headerNames) <= i {
|
|
||||||
headerNames = append(headerNames, name)
|
|
||||||
} else {
|
|
||||||
headerNames[i] = headerNames[i] + name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.ColumnNames = headerNames[p.SkipColumns:]
|
|
||||||
} else {
|
|
||||||
// if columns are named, just skip header rows
|
|
||||||
for i := 0; i < p.HeaderRowCount; i++ {
|
|
||||||
_, err := csvReader.Read()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table, err := csvReader.ReadAll()
|
table, err := csvReader.ReadAll()
|
||||||
|
|
@ -156,27 +176,6 @@ func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
|
||||||
return metrics, nil
|
return metrics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseLine does not use any information in header and assumes DataColumns is set
|
|
||||||
// it will also not skip any rows
|
|
||||||
func (p *Parser) ParseLine(line string) (telegraf.Metric, error) {
|
|
||||||
r := bytes.NewReader([]byte(line))
|
|
||||||
csvReader := p.compile(r)
|
|
||||||
// if there is nothing in DataColumns, ParseLine will fail
|
|
||||||
if len(p.ColumnNames) == 0 {
|
|
||||||
return nil, fmt.Errorf("[parsers.csv] data columns must be specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
record, err := csvReader.Read()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m, err := p.parseRecord(record)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Parser) parseRecord(record []string) (telegraf.Metric, error) {
|
func (p *Parser) parseRecord(record []string) (telegraf.Metric, error) {
|
||||||
recordFields := make(map[string]interface{})
|
recordFields := make(map[string]interface{})
|
||||||
tags := make(map[string]string)
|
tags := make(map[string]string)
|
||||||
|
|
@ -289,7 +288,7 @@ outer:
|
||||||
// will be the current timestamp, else it will try to parse the time according
|
// will be the current timestamp, else it will try to parse the time according
|
||||||
// to the format.
|
// to the format.
|
||||||
func parseTimestamp(timeFunc func() time.Time, recordFields map[string]interface{},
|
func parseTimestamp(timeFunc func() time.Time, recordFields map[string]interface{},
|
||||||
timestampColumn, timestampFormat string, Timezone string,
|
timestampColumn, timestampFormat string, timezone string,
|
||||||
) (time.Time, error) {
|
) (time.Time, error) {
|
||||||
if timestampColumn != "" {
|
if timestampColumn != "" {
|
||||||
if recordFields[timestampColumn] == nil {
|
if recordFields[timestampColumn] == nil {
|
||||||
|
|
@ -300,7 +299,7 @@ func parseTimestamp(timeFunc func() time.Time, recordFields map[string]interface
|
||||||
case "":
|
case "":
|
||||||
return time.Time{}, fmt.Errorf("timestamp format must be specified")
|
return time.Time{}, fmt.Errorf("timestamp format must be specified")
|
||||||
default:
|
default:
|
||||||
metricTime, err := internal.ParseTimestamp(timestampFormat, recordFields[timestampColumn], Timezone)
|
metricTime, err := internal.ParseTimestamp(timestampFormat, recordFields[timestampColumn], timezone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, err
|
return time.Time{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package csv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -59,9 +60,33 @@ func TestHeaderOverride(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testCSV := `line1,line2,line3
|
testCSV := `line1,line2,line3
|
||||||
3.4,70,test_name`
|
3.4,70,test_name`
|
||||||
|
expectedFields := map[string]interface{}{
|
||||||
|
"first": 3.4,
|
||||||
|
"second": int64(70),
|
||||||
|
}
|
||||||
metrics, err := p.Parse([]byte(testCSV))
|
metrics, err := p.Parse([]byte(testCSV))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "test_name", metrics[0].Name())
|
require.Equal(t, "test_name", metrics[0].Name())
|
||||||
|
require.Equal(t, expectedFields, metrics[0].Fields())
|
||||||
|
|
||||||
|
testCSVRows := []string{"line1,line2,line3\r\n", "3.4,70,test_name\r\n"}
|
||||||
|
|
||||||
|
p, err = NewParser(
|
||||||
|
&Config{
|
||||||
|
HeaderRowCount: 1,
|
||||||
|
ColumnNames: []string{"first", "second", "third"},
|
||||||
|
MeasurementColumn: "third",
|
||||||
|
TimeFunc: DefaultTime,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
metrics, err = p.Parse([]byte(testCSVRows[0]))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []telegraf.Metric{}, metrics)
|
||||||
|
m, err := p.ParseLine(testCSVRows[1])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test_name", m.Name())
|
||||||
|
require.Equal(t, expectedFields, m.Fields())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimestamp(t *testing.T) {
|
func TestTimestamp(t *testing.T) {
|
||||||
|
|
@ -293,6 +318,22 @@ func TestTrimSpace(t *testing.T) {
|
||||||
metrics, err := p.Parse([]byte(testCSV))
|
metrics, err := p.Parse([]byte(testCSV))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, expectedFields, metrics[0].Fields())
|
require.Equal(t, expectedFields, metrics[0].Fields())
|
||||||
|
|
||||||
|
p, err = NewParser(
|
||||||
|
&Config{
|
||||||
|
HeaderRowCount: 2,
|
||||||
|
TrimSpace: true,
|
||||||
|
TimeFunc: DefaultTime,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
testCSV = " col , col ,col\n" +
|
||||||
|
" 1 , 2 ,3\n" +
|
||||||
|
" test space , 80 ,test_name"
|
||||||
|
|
||||||
|
metrics, err = p.Parse([]byte(testCSV))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, map[string]interface{}{"col1": "test space", "col2": int64(80), "col3": "test_name"}, metrics[0].Fields())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrimSpaceDelimitedBySpace(t *testing.T) {
|
func TestTrimSpaceDelimitedBySpace(t *testing.T) {
|
||||||
|
|
@ -332,6 +373,7 @@ func TestSkipRows(t *testing.T) {
|
||||||
TimeFunc: DefaultTime,
|
TimeFunc: DefaultTime,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
testCSV := `garbage nonsense
|
testCSV := `garbage nonsense
|
||||||
line1,line2,line3
|
line1,line2,line3
|
||||||
hello,80,test_name2`
|
hello,80,test_name2`
|
||||||
|
|
@ -339,10 +381,39 @@ hello,80,test_name2`
|
||||||
expectedFields := map[string]interface{}{
|
expectedFields := map[string]interface{}{
|
||||||
"line2": int64(80),
|
"line2": int64(80),
|
||||||
}
|
}
|
||||||
|
expectedTags := map[string]string{
|
||||||
|
"line1": "hello",
|
||||||
|
}
|
||||||
metrics, err := p.Parse([]byte(testCSV))
|
metrics, err := p.Parse([]byte(testCSV))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "test_name2", metrics[0].Name())
|
require.Equal(t, "test_name2", metrics[0].Name())
|
||||||
require.Equal(t, expectedFields, metrics[0].Fields())
|
require.Equal(t, expectedFields, metrics[0].Fields())
|
||||||
|
require.Equal(t, expectedTags, metrics[0].Tags())
|
||||||
|
|
||||||
|
p, err = NewParser(
|
||||||
|
&Config{
|
||||||
|
HeaderRowCount: 1,
|
||||||
|
SkipRows: 1,
|
||||||
|
TagColumns: []string{"line1"},
|
||||||
|
MeasurementColumn: "line3",
|
||||||
|
TimeFunc: DefaultTime,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
testCSVRows := []string{"garbage nonsense\r\n", "line1,line2,line3\r\n", "hello,80,test_name2\r\n"}
|
||||||
|
|
||||||
|
metrics, err = p.Parse([]byte(testCSVRows[0]))
|
||||||
|
require.Error(t, io.EOF, err)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, metrics)
|
||||||
|
m, err := p.ParseLine(testCSVRows[1])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, m)
|
||||||
|
m, err = p.ParseLine(testCSVRows[2])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test_name2", m.Name())
|
||||||
|
require.Equal(t, expectedFields, m.Fields())
|
||||||
|
require.Equal(t, expectedTags, m.Tags())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSkipColumns(t *testing.T) {
|
func TestSkipColumns(t *testing.T) {
|
||||||
|
|
@ -375,8 +446,8 @@ func TestSkipColumnsWithHeader(t *testing.T) {
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testCSV := `col,col,col
|
testCSV := `col,col,col
|
||||||
1,2,3
|
1,2,3
|
||||||
trash,80,test_name`
|
trash,80,test_name`
|
||||||
|
|
||||||
// we should expect an error if we try to get col1
|
// we should expect an error if we try to get col1
|
||||||
metrics, err := p.Parse([]byte(testCSV))
|
metrics, err := p.Parse([]byte(testCSV))
|
||||||
|
|
@ -384,6 +455,44 @@ func TestSkipColumnsWithHeader(t *testing.T) {
|
||||||
require.Equal(t, map[string]interface{}{"col2": int64(80), "col3": "test_name"}, metrics[0].Fields())
|
require.Equal(t, map[string]interface{}{"col2": int64(80), "col3": "test_name"}, metrics[0].Fields())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMultiHeader(t *testing.T) {
|
||||||
|
p, err := NewParser(
|
||||||
|
&Config{
|
||||||
|
HeaderRowCount: 2,
|
||||||
|
TimeFunc: DefaultTime,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
testCSV := `col,col
|
||||||
|
1,2
|
||||||
|
80,test_name`
|
||||||
|
|
||||||
|
metrics, err := p.Parse([]byte(testCSV))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, map[string]interface{}{"col1": int64(80), "col2": "test_name"}, metrics[0].Fields())
|
||||||
|
|
||||||
|
testCSVRows := []string{"col,col\r\n", "1,2\r\n", "80,test_name\r\n"}
|
||||||
|
|
||||||
|
p, err = NewParser(
|
||||||
|
&Config{
|
||||||
|
HeaderRowCount: 2,
|
||||||
|
TimeFunc: DefaultTime,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
metrics, err = p.Parse([]byte(testCSVRows[0]))
|
||||||
|
require.Error(t, io.EOF, err)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, metrics)
|
||||||
|
m, err := p.ParseLine(testCSVRows[1])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, m)
|
||||||
|
m, err = p.ParseLine(testCSVRows[2])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, map[string]interface{}{"col1": int64(80), "col2": "test_name"}, m.Fields())
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseStream(t *testing.T) {
|
func TestParseStream(t *testing.T) {
|
||||||
p, err := NewParser(
|
p, err := NewParser(
|
||||||
&Config{
|
&Config{
|
||||||
|
|
@ -400,7 +509,8 @@ func TestParseStream(t *testing.T) {
|
||||||
metrics, err := p.Parse([]byte(csvHeader))
|
metrics, err := p.Parse([]byte(csvHeader))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, metrics, 0)
|
require.Len(t, metrics, 0)
|
||||||
metric, err := p.ParseLine(csvBody)
|
m, err := p.ParseLine(csvBody)
|
||||||
|
require.NoError(t, err)
|
||||||
testutil.RequireMetricEqual(t,
|
testutil.RequireMetricEqual(t,
|
||||||
testutil.MustMetric(
|
testutil.MustMetric(
|
||||||
"csv",
|
"csv",
|
||||||
|
|
@ -411,7 +521,45 @@ func TestParseStream(t *testing.T) {
|
||||||
"c": int64(3),
|
"c": int64(3),
|
||||||
},
|
},
|
||||||
DefaultTime(),
|
DefaultTime(),
|
||||||
), metric)
|
), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseLineMultiMetricErrorMessage(t *testing.T) {
|
||||||
|
p, err := NewParser(
|
||||||
|
&Config{
|
||||||
|
MetricName: "csv",
|
||||||
|
HeaderRowCount: 1,
|
||||||
|
TimeFunc: DefaultTime,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
csvHeader := "a,b,c"
|
||||||
|
csvOneRow := "1,2,3"
|
||||||
|
csvTwoRows := "4,5,6\n7,8,9"
|
||||||
|
|
||||||
|
metrics, err := p.Parse([]byte(csvHeader))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, metrics, 0)
|
||||||
|
m, err := p.ParseLine(csvOneRow)
|
||||||
|
require.NoError(t, err)
|
||||||
|
testutil.RequireMetricEqual(t,
|
||||||
|
testutil.MustMetric(
|
||||||
|
"csv",
|
||||||
|
map[string]string{},
|
||||||
|
map[string]interface{}{
|
||||||
|
"a": int64(1),
|
||||||
|
"b": int64(2),
|
||||||
|
"c": int64(3),
|
||||||
|
},
|
||||||
|
DefaultTime(),
|
||||||
|
), m)
|
||||||
|
m, err = p.ParseLine(csvTwoRows)
|
||||||
|
require.Errorf(t, err, "expected 1 metric found 2")
|
||||||
|
require.Nil(t, m)
|
||||||
|
metrics, err = p.Parse([]byte(csvTwoRows))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, metrics, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimestampUnixFloatPrecision(t *testing.T) {
|
func TestTimestampUnixFloatPrecision(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue