Support configuring a default timezone in JSON parser (#5472)

This commit is contained in:
Douglas Drinka 2019-02-25 12:30:33 -07:00 committed by Daniel Nelson
parent eb794ec30f
commit 1886676e14
7 changed files with 95 additions and 4 deletions

View File

@ -1362,6 +1362,14 @@ func getParserConfig(name string, tbl *ast.Table) (*parsers.Config, error) {
} }
} }
if node, ok := tbl.Fields["json_timezone"]; ok {
if kv, ok := node.(*ast.KeyValue); ok {
if str, ok := kv.Value.(*ast.String); ok {
c.JSONTimezone = str.Value
}
}
}
if node, ok := tbl.Fields["data_type"]; ok { if node, ok := tbl.Fields["data_type"]; ok {
if kv, ok := node.(*ast.KeyValue); ok { if kv, ok := node.(*ast.KeyValue); ok {
if str, ok := kv.Value.(*ast.String); ok { if str, ok := kv.Value.(*ast.String); ok {
@ -1637,6 +1645,7 @@ func getParserConfig(name string, tbl *ast.Table) (*parsers.Config, error) {
delete(tbl.Fields, "json_string_fields") delete(tbl.Fields, "json_string_fields")
delete(tbl.Fields, "json_time_format") delete(tbl.Fields, "json_time_format")
delete(tbl.Fields, "json_time_key") delete(tbl.Fields, "json_time_key")
delete(tbl.Fields, "json_timezone")
delete(tbl.Fields, "data_type") delete(tbl.Fields, "data_type")
delete(tbl.Fields, "collectd_auth_file") delete(tbl.Fields, "collectd_auth_file")
delete(tbl.Fields, "collectd_security_level") delete(tbl.Fields, "collectd_security_level")

View File

@ -333,13 +333,18 @@ func CompressWithGzip(data io.Reader) (io.Reader, error) {
return pipeReader, err return pipeReader, err
} }
// ParseTimestamp with no location provided parses a timestamp value as UTC
func ParseTimestamp(timestamp interface{}, format string) (time.Time, error) {
return ParseTimestampWithLocation(timestamp, format, "UTC")
}
// ParseTimestamp parses a timestamp value as a unix epoch of various precision. // ParseTimestamp parses a timestamp value as a unix epoch of various precision.
// //
// format = "unix": epoch is assumed to be in seconds and can come as number or string. Can have a decimal part. // format = "unix": epoch is assumed to be in seconds and can come as number or string. Can have a decimal part.
// format = "unix_ms": epoch is assumed to be in milliseconds and can come as number or string. Cannot have a decimal part. // format = "unix_ms": epoch is assumed to be in milliseconds and can come as number or string. Cannot have a decimal part.
// format = "unix_us": epoch is assumed to be in microseconds and can come as number or string. Cannot have a decimal part. // format = "unix_us": epoch is assumed to be in microseconds and can come as number or string. Cannot have a decimal part.
// format = "unix_ns": epoch is assumed to be in nanoseconds and can come as number or string. Cannot have a decimal part. // format = "unix_ns": epoch is assumed to be in nanoseconds and can come as number or string. Cannot have a decimal part.
func ParseTimestamp(timestamp interface{}, format string) (time.Time, error) { func ParseTimestampWithLocation(timestamp interface{}, format string, location string) (time.Time, error) {
timeInt, timeFractional := int64(0), int64(0) timeInt, timeFractional := int64(0), int64(0)
timeEpochStr, ok := timestamp.(string) timeEpochStr, ok := timestamp.(string)
var err error var err error
@ -355,7 +360,11 @@ func ParseTimestamp(timestamp interface{}, format string) (time.Time, error) {
splitted := regexp.MustCompile("[.,]").Split(timeEpochStr, 2) splitted := regexp.MustCompile("[.,]").Split(timeEpochStr, 2)
timeInt, err = strconv.ParseInt(splitted[0], 10, 64) timeInt, err = strconv.ParseInt(splitted[0], 10, 64)
if err != nil { if err != nil {
return time.Parse(format, timeEpochStr) loc, err := time.LoadLocation(location)
if err != nil {
return time.Time{}, fmt.Errorf("location: %s could not be loaded as a location", location)
}
return time.ParseInLocation(format, timeEpochStr, loc)
} }
if len(splitted) == 2 { if len(splitted) == 2 {

View File

@ -270,3 +270,34 @@ func TestAlignDuration(t *testing.T) {
}) })
} }
} }
func TestParseTimestamp(t *testing.T) {
time, err := ParseTimestamp("2019-02-20 21:50:34.029665", "2006-01-02 15:04:05.000000")
assert.Nil(t, err)
assert.EqualValues(t, int64(1550699434029665000), time.UnixNano())
time, err = ParseTimestamp("2019-02-20 21:50:34.029665-04:00", "2006-01-02 15:04:05.000000-07:00")
assert.Nil(t, err)
assert.EqualValues(t, int64(1550713834029665000), time.UnixNano())
time, err = ParseTimestamp("2019-02-20 21:50:34.029665", "2006-01-02 15:04:05.000000-06:00")
assert.NotNil(t, err)
}
func TestParseTimestampWithLocation(t *testing.T) {
time, err := ParseTimestampWithLocation("2019-02-20 21:50:34.029665", "2006-01-02 15:04:05.000000", "UTC")
assert.Nil(t, err)
assert.EqualValues(t, int64(1550699434029665000), time.UnixNano())
time, err = ParseTimestampWithLocation("2019-02-20 21:50:34.029665", "2006-01-02 15:04:05.000000", "America/New_York")
assert.Nil(t, err)
assert.EqualValues(t, int64(1550717434029665000), time.UnixNano())
//Provided location is ignored if an offset is successfully parsed
time, err = ParseTimestampWithLocation("2019-02-20 21:50:34.029665-07:00", "2006-01-02 15:04:05.000000-07:00", "America/New_York")
assert.Nil(t, err)
assert.EqualValues(t, int64(1550724634029665000), time.UnixNano())
time, err = ParseTimestampWithLocation("2019-02-20 21:50:34.029665", "2006-01-02 15:04:05.000000", "InvalidTimeZone")
assert.NotNil(t, err)
}

View File

@ -49,9 +49,21 @@ ignored unless specified in the `tag_key` or `json_string_fields` options.
## https://golang.org/pkg/time/#Time.Format ## https://golang.org/pkg/time/#Time.Format
## ex: json_time_format = "Mon Jan 2 15:04:05 -0700 MST 2006" ## ex: json_time_format = "Mon Jan 2 15:04:05 -0700 MST 2006"
## json_time_format = "2006-01-02T15:04:05Z07:00" ## json_time_format = "2006-01-02T15:04:05Z07:00"
## json_time_format = "01/02/2006 15:04:05"
## json_time_format = "unix" ## json_time_format = "unix"
## json_time_format = "unix_ms" ## json_time_format = "unix_ms"
json_time_format = "" json_time_format = ""
## Timezone allows you to provide an override for timestamps that
## don't already include an offset
## e.g. 04/06/2016 12:41:45
##
## Default: "" which renders UTC
## Options are as follows:
## 1. Local -- interpret based on machine localtime
## 2. "America/New_York" -- Unix TZ values like those found in https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
## 3. UTC -- or blank/unspecified, will return timestamp in UTC
json_timezone = ""
``` ```
#### json_query #### json_query
@ -62,7 +74,7 @@ query should contain a JSON object or an array of objects.
Consult the GJSON [path syntax][gjson syntax] for details and examples. Consult the GJSON [path syntax][gjson syntax] for details and examples.
#### json_time_key, json_time_format #### json_time_key, json_time_format, json_timezone
By default the current time will be used for all created metrics, to set the By default the current time will be used for all created metrics, to set the
time using the JSON document you can use the `json_time_key` and time using the JSON document you can use the `json_time_key` and
@ -77,6 +89,12 @@ the Go "reference time" which is defined to be the specific time:
Consult the Go [time][time parse] package for details and additional examples Consult the Go [time][time parse] package for details and additional examples
on how to set the time format. on how to set the time format.
When parsing times that don't include a timezone specifier, times are assumed
to be UTC. To default to another timezone, or to local time, specify the
`json_timezone` option. This option should be set to a
[Unix TZ value](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones),
such as `America/New_York`, to `Local` to utilize the system timezone, or to `UTC`.
### Examples ### Examples
#### Basic Parsing #### Basic Parsing

View File

@ -28,6 +28,7 @@ type JSONParser struct {
JSONQuery string JSONQuery string
JSONTimeKey string JSONTimeKey string
JSONTimeFormat string JSONTimeFormat string
JSONTimezone string
DefaultTags map[string]string DefaultTags map[string]string
} }
@ -82,7 +83,7 @@ func (p *JSONParser) parseObject(metrics []telegraf.Metric, jsonOut map[string]i
return nil, err return nil, err
} }
nTime, err = internal.ParseTimestamp(f.Fields[p.JSONTimeKey], p.JSONTimeFormat) nTime, err = internal.ParseTimestampWithLocation(f.Fields[p.JSONTimeKey], p.JSONTimeFormat, p.JSONTimezone)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -599,6 +599,23 @@ func TestTimeParser(t *testing.T) {
require.Equal(t, false, metrics[0].Time() == metrics[1].Time()) require.Equal(t, false, metrics[0].Time() == metrics[1].Time())
} }
func TestTimeParserWithTimezone(t *testing.T) {
testString := `{
"time": "04 Jan 06 15:04"
}`
parser := JSONParser{
MetricName: "json_test",
JSONTimeKey: "time",
JSONTimeFormat: "02 Jan 06 15:04",
JSONTimezone: "America/New_York",
}
metrics, err := parser.Parse([]byte(testString))
require.NoError(t, err)
require.Equal(t, 1, len(metrics))
require.EqualValues(t, int64(1136405040000000000), metrics[0].Time().UnixNano())
}
func TestUnixTimeParser(t *testing.T) { func TestUnixTimeParser(t *testing.T) {
testString := `[ testString := `[
{ {

View File

@ -85,6 +85,9 @@ type Config struct {
// time format // time format
JSONTimeFormat string `toml:"json_time_format"` JSONTimeFormat string `toml:"json_time_format"`
// default timezone
JSONTimezone string `toml:"json_timezone"`
// Authentication file for collectd // Authentication file for collectd
CollectdAuthFile string `toml:"collectd_auth_file"` CollectdAuthFile string `toml:"collectd_auth_file"`
// One of none (default), sign, or encrypt // One of none (default), sign, or encrypt
@ -152,6 +155,7 @@ func NewParser(config *Config) (Parser, error) {
config.JSONQuery, config.JSONQuery,
config.JSONTimeKey, config.JSONTimeKey,
config.JSONTimeFormat, config.JSONTimeFormat,
config.JSONTimezone,
config.DefaultTags) config.DefaultTags)
case "value": case "value":
parser, err = NewValueParser(config.MetricName, parser, err = NewValueParser(config.MetricName,
@ -275,6 +279,7 @@ func newJSONParser(
jsonQuery string, jsonQuery string,
timeKey string, timeKey string,
timeFormat string, timeFormat string,
timezone string,
defaultTags map[string]string, defaultTags map[string]string,
) Parser { ) Parser {
parser := &json.JSONParser{ parser := &json.JSONParser{
@ -285,6 +290,7 @@ func newJSONParser(
JSONQuery: jsonQuery, JSONQuery: jsonQuery,
JSONTimeKey: timeKey, JSONTimeKey: timeKey,
JSONTimeFormat: timeFormat, JSONTimeFormat: timeFormat,
JSONTimezone: timezone,
DefaultTags: defaultTags, DefaultTags: defaultTags,
} }
return parser return parser