github.com/observiq/carbon@v0.9.11-0.20200820160507-1b872e368a5e/operator/builtin/parser/time_test.go (about)

     1  package parser
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/observiq/carbon/entry"
    10  	"github.com/observiq/carbon/operator"
    11  	"github.com/observiq/carbon/operator/helper"
    12  	"github.com/observiq/carbon/testutil"
    13  	"github.com/stretchr/testify/mock"
    14  	"github.com/stretchr/testify/require"
    15  )
    16  
    17  func TestIsZero(t *testing.T) {
    18  	require.True(t, (&helper.TimeParser{}).IsZero())
    19  	require.False(t, (&helper.TimeParser{Layout: "strptime"}).IsZero())
    20  }
    21  
    22  func TestTimeParser(t *testing.T) {
    23  
    24  	testCases := []struct {
    25  		name           string
    26  		sample         interface{}
    27  		gotimeLayout   string
    28  		strptimeLayout string
    29  	}{
    30  		{
    31  			name:           "unix",
    32  			sample:         "Mon Jan 2 15:04:05 MST 2006",
    33  			gotimeLayout:   "Mon Jan 2 15:04:05 MST 2006",
    34  			strptimeLayout: "%a %b %e %H:%M:%S %Z %Y",
    35  		},
    36  		{
    37  			name:           "almost-unix",
    38  			sample:         "Mon Jan 02 15:04:05 MST 2006",
    39  			gotimeLayout:   "Mon Jan 02 15:04:05 MST 2006",
    40  			strptimeLayout: "%a %b %d %H:%M:%S %Z %Y",
    41  		},
    42  		{
    43  			name:           "kitchen",
    44  			sample:         "12:34PM",
    45  			gotimeLayout:   time.Kitchen,
    46  			strptimeLayout: "%H:%M%p",
    47  		},
    48  		{
    49  			name:           "kitchen-bytes",
    50  			sample:         []byte("12:34PM"),
    51  			gotimeLayout:   time.Kitchen,
    52  			strptimeLayout: "%H:%M%p",
    53  		},
    54  		{
    55  			name:           "countdown",
    56  			sample:         "-0100 01 01 01 01 01 01",
    57  			gotimeLayout:   "-0700 06 05 04 03 02 01",
    58  			strptimeLayout: "%z %y %S %M %H %e %m",
    59  		},
    60  		{
    61  			name:           "debian-syslog",
    62  			sample:         "Jun 09 11:39:45",
    63  			gotimeLayout:   "Jan 02 15:04:05",
    64  			strptimeLayout: "%b %d %H:%M:%S",
    65  		},
    66  		{
    67  			name:           "opendistro",
    68  			sample:         "2020-06-09T15:39:58",
    69  			gotimeLayout:   "2006-01-02T15:04:05",
    70  			strptimeLayout: "%Y-%m-%dT%H:%M:%S",
    71  		},
    72  		{
    73  			name:           "postgres",
    74  			sample:         "2019-11-05 10:38:35.118 EST",
    75  			gotimeLayout:   "2006-01-02 15:04:05.999 MST",
    76  			strptimeLayout: "%Y-%m-%d %H:%M:%S.%L %Z",
    77  		},
    78  		{
    79  			name:           "ibm-mq",
    80  			sample:         "3/4/2018 11:52:29",
    81  			gotimeLayout:   "1/2/2006 15:04:05",
    82  			strptimeLayout: "%q/%g/%Y %H:%M:%S",
    83  		},
    84  		{
    85  			name:           "cassandra",
    86  			sample:         "2019-11-27T09:34:32.901-0500",
    87  			gotimeLayout:   "2006-01-02T15:04:05.999-0700",
    88  			strptimeLayout: "%Y-%m-%dT%H:%M:%S.%L%z",
    89  		},
    90  		{
    91  			name:           "oracle",
    92  			sample:         "2019-10-15T10:42:01.900436-04:00",
    93  			gotimeLayout:   "2006-01-02T15:04:05.999999-07:00",
    94  			strptimeLayout: "%Y-%m-%dT%H:%M:%S.%f%j",
    95  		},
    96  		{
    97  			name:           "oracle-listener",
    98  			sample:         "22-JUL-2019 15:16:13",
    99  			gotimeLayout:   "02-Jan-2006 15:04:05",
   100  			strptimeLayout: "%d-%b-%Y %H:%M:%S",
   101  		},
   102  		{
   103  			name:           "k8s",
   104  			sample:         "2019-03-08T18:41:12.152531115Z",
   105  			gotimeLayout:   "2006-01-02T15:04:05.999999999Z",
   106  			strptimeLayout: "%Y-%m-%dT%H:%M:%S.%sZ",
   107  		},
   108  		{
   109  			name:           "jetty",
   110  			sample:         "05/Aug/2019:20:38:46 +0000",
   111  			gotimeLayout:   "02/Jan/2006:15:04:05 -0700",
   112  			strptimeLayout: "%d/%b/%Y:%H:%M:%S %z",
   113  		},
   114  		{
   115  			name:           "puppet",
   116  			sample:         "Aug  4 03:26:02",
   117  			gotimeLayout:   "Jan _2 15:04:05",
   118  			strptimeLayout: "%b %e %H:%M:%S",
   119  		},
   120  	}
   121  
   122  	rootField := entry.NewRecordField()
   123  	someField := entry.NewRecordField("some_field")
   124  
   125  	for _, tc := range testCases {
   126  		t.Run(tc.name, func(t *testing.T) {
   127  			var sampleStr string
   128  			switch s := tc.sample.(type) {
   129  			case string:
   130  				sampleStr = s
   131  			case []byte:
   132  				sampleStr = string(s)
   133  			default:
   134  				require.FailNow(t, "unexpected sample type")
   135  			}
   136  
   137  			expected, err := time.ParseInLocation(tc.gotimeLayout, sampleStr, time.Local)
   138  			require.NoError(t, err, "Test configuration includes invalid timestamp or layout")
   139  
   140  			gotimeRootCfg := parseTimeTestConfig(helper.GotimeKey, tc.gotimeLayout, rootField)
   141  			t.Run("gotime-root", runTimeParseTest(t, gotimeRootCfg, makeTestEntry(rootField, tc.sample), false, false, expected))
   142  
   143  			gotimeNonRootCfg := parseTimeTestConfig(helper.GotimeKey, tc.gotimeLayout, someField)
   144  			t.Run("gotime-non-root", runTimeParseTest(t, gotimeNonRootCfg, makeTestEntry(someField, tc.sample), false, false, expected))
   145  
   146  			strptimeRootCfg := parseTimeTestConfig(helper.StrptimeKey, tc.strptimeLayout, rootField)
   147  			t.Run("strptime-root", runTimeParseTest(t, strptimeRootCfg, makeTestEntry(rootField, tc.sample), false, false, expected))
   148  
   149  			strptimeNonRootCfg := parseTimeTestConfig(helper.StrptimeKey, tc.strptimeLayout, someField)
   150  			t.Run("strptime-non-root", runTimeParseTest(t, strptimeNonRootCfg, makeTestEntry(someField, tc.sample), false, false, expected))
   151  		})
   152  	}
   153  }
   154  
   155  func TestTimeEpochs(t *testing.T) {
   156  
   157  	testCases := []struct {
   158  		name     string
   159  		sample   interface{}
   160  		layout   string
   161  		expected time.Time
   162  		maxLoss  time.Duration
   163  	}{
   164  		{
   165  			name:     "s-default-string",
   166  			sample:   "1136214245",
   167  			layout:   "s",
   168  			expected: time.Unix(1136214245, 0),
   169  		},
   170  		{
   171  			name:     "s-default-bytes",
   172  			sample:   []byte("1136214245"),
   173  			layout:   "s",
   174  			expected: time.Unix(1136214245, 0),
   175  		},
   176  		{
   177  			name:     "s-default-int",
   178  			sample:   1136214245,
   179  			layout:   "s",
   180  			expected: time.Unix(1136214245, 0),
   181  		},
   182  		{
   183  			name:     "s-default-float",
   184  			sample:   1136214245.0,
   185  			layout:   "s",
   186  			expected: time.Unix(1136214245, 0),
   187  		},
   188  		{
   189  			name:     "ms-default-string",
   190  			sample:   "1136214245123",
   191  			layout:   "ms",
   192  			expected: time.Unix(1136214245, 123000000),
   193  		},
   194  		{
   195  			name:     "ms-default-int",
   196  			sample:   1136214245123,
   197  			layout:   "ms",
   198  			expected: time.Unix(1136214245, 123000000),
   199  		},
   200  		{
   201  			name:     "ms-default-float",
   202  			sample:   1136214245123.0,
   203  			layout:   "ms",
   204  			expected: time.Unix(1136214245, 123000000),
   205  		},
   206  		{
   207  			name:     "us-default-string",
   208  			sample:   "1136214245123456",
   209  			layout:   "us",
   210  			expected: time.Unix(1136214245, 123456000),
   211  		},
   212  		{
   213  			name:     "us-default-int",
   214  			sample:   1136214245123456,
   215  			layout:   "us",
   216  			expected: time.Unix(1136214245, 123456000),
   217  		},
   218  		{
   219  			name:     "us-default-float",
   220  			sample:   1136214245123456.0,
   221  			layout:   "us",
   222  			expected: time.Unix(1136214245, 123456000),
   223  		},
   224  		{
   225  			name:     "ns-default-string",
   226  			sample:   "1136214245123456789",
   227  			layout:   "ns",
   228  			expected: time.Unix(1136214245, 123456789),
   229  		},
   230  		{
   231  			name:     "ns-default-int",
   232  			sample:   1136214245123456789,
   233  			layout:   "ns",
   234  			expected: time.Unix(1136214245, 123456789),
   235  		},
   236  		{
   237  			name:     "ns-default-float",
   238  			sample:   1136214245123456789.0,
   239  			layout:   "ns",
   240  			expected: time.Unix(1136214245, 123456789),
   241  			maxLoss:  time.Nanosecond * 100,
   242  		},
   243  		{
   244  			name:     "s.ms-default-string",
   245  			sample:   "1136214245.123",
   246  			layout:   "s.ms",
   247  			expected: time.Unix(1136214245, 123000000),
   248  		},
   249  		{
   250  			name:     "s.ms-default-int",
   251  			sample:   1136214245,
   252  			layout:   "s.ms",
   253  			expected: time.Unix(1136214245, 0), // drops subseconds
   254  			maxLoss:  time.Nanosecond * 100,
   255  		},
   256  		{
   257  			name:     "s.ms-default-float",
   258  			sample:   1136214245.123,
   259  			layout:   "s.ms",
   260  			expected: time.Unix(1136214245, 123000000),
   261  		},
   262  		{
   263  			name:     "s.us-default-string",
   264  			sample:   "1136214245.123456",
   265  			layout:   "s.us",
   266  			expected: time.Unix(1136214245, 123456000),
   267  		},
   268  		{
   269  			name:     "s.us-default-int",
   270  			sample:   1136214245,
   271  			layout:   "s.us",
   272  			expected: time.Unix(1136214245, 0), // drops subseconds
   273  			maxLoss:  time.Nanosecond * 100,
   274  		},
   275  		{
   276  			name:     "s.us-default-float",
   277  			sample:   1136214245.123456,
   278  			layout:   "s.us",
   279  			expected: time.Unix(1136214245, 123456000),
   280  		},
   281  		{
   282  			name:     "s.ns-default-string",
   283  			sample:   "1136214245.123456789",
   284  			layout:   "s.ns",
   285  			expected: time.Unix(1136214245, 123456789),
   286  		},
   287  		{
   288  			name:     "s.ns-default-int",
   289  			sample:   1136214245,
   290  			layout:   "s.ns",
   291  			expected: time.Unix(1136214245, 0), // drops subseconds
   292  			maxLoss:  time.Nanosecond * 100,
   293  		},
   294  		{
   295  			name:     "s.ns-default-float",
   296  			sample:   1136214245.123456789,
   297  			layout:   "s.ns",
   298  			expected: time.Unix(1136214245, 123456789),
   299  			maxLoss:  time.Nanosecond * 100,
   300  		},
   301  	}
   302  
   303  	rootField := entry.NewRecordField()
   304  	someField := entry.NewRecordField("some_field")
   305  
   306  	for _, tc := range testCases {
   307  		t.Run(tc.name, func(t *testing.T) {
   308  			rootCfg := parseTimeTestConfig(helper.EpochKey, tc.layout, rootField)
   309  			t.Run("epoch-root", runLossyTimeParseTest(t, rootCfg, makeTestEntry(rootField, tc.sample), false, false, tc.expected, tc.maxLoss))
   310  
   311  			nonRootCfg := parseTimeTestConfig(helper.EpochKey, tc.layout, someField)
   312  			t.Run("epoch-non-root", runLossyTimeParseTest(t, nonRootCfg, makeTestEntry(someField, tc.sample), false, false, tc.expected, tc.maxLoss))
   313  		})
   314  	}
   315  }
   316  
   317  func TestTimeErrors(t *testing.T) {
   318  
   319  	testCases := []struct {
   320  		name       string
   321  		sample     interface{}
   322  		layoutType string
   323  		layout     string
   324  		buildErr   bool
   325  		parseErr   bool
   326  	}{
   327  		{
   328  			name:       "bad-layout-type",
   329  			layoutType: "fake",
   330  			buildErr:   true,
   331  		},
   332  		{
   333  			name:       "bad-strptime-directive",
   334  			layoutType: "strptime",
   335  			layout:     "%1",
   336  			buildErr:   true,
   337  		},
   338  		{
   339  			name:       "bad-epoch-layout",
   340  			layoutType: "epoch",
   341  			layout:     "years",
   342  			buildErr:   true,
   343  		},
   344  		{
   345  			name:       "bad-native-value",
   346  			layoutType: "native",
   347  			sample:     1,
   348  			parseErr:   true,
   349  		},
   350  		{
   351  			name:       "bad-gotime-value",
   352  			layoutType: "gotime",
   353  			layout:     time.Kitchen,
   354  			sample:     1,
   355  			parseErr:   true,
   356  		},
   357  		{
   358  			name:       "bad-epoch-value",
   359  			layoutType: "epoch",
   360  			layout:     "s",
   361  			sample:     "not-a-number",
   362  			parseErr:   true,
   363  		},
   364  	}
   365  
   366  	rootField := entry.NewRecordField()
   367  	someField := entry.NewRecordField("some_field")
   368  
   369  	for _, tc := range testCases {
   370  		t.Run(tc.name, func(t *testing.T) {
   371  			rootCfg := parseTimeTestConfig(tc.layoutType, tc.layout, rootField)
   372  			t.Run("err-root", runTimeParseTest(t, rootCfg, makeTestEntry(rootField, tc.sample), tc.buildErr, tc.parseErr, time.Now()))
   373  
   374  			nonRootCfg := parseTimeTestConfig(tc.layoutType, tc.layout, someField)
   375  			t.Run("err-non-root", runTimeParseTest(t, nonRootCfg, makeTestEntry(someField, tc.sample), tc.buildErr, tc.parseErr, time.Now()))
   376  		})
   377  	}
   378  }
   379  
   380  func makeTestEntry(field entry.Field, value interface{}) *entry.Entry {
   381  	e := entry.New()
   382  	e.Set(field, value)
   383  	return e
   384  }
   385  
   386  func runTimeParseTest(t *testing.T, cfg *TimeParserConfig, ent *entry.Entry, buildErr bool, parseErr bool, expected time.Time) func(*testing.T) {
   387  	return runLossyTimeParseTest(t, cfg, ent, buildErr, parseErr, expected, time.Duration(0))
   388  }
   389  
   390  func runLossyTimeParseTest(t *testing.T, cfg *TimeParserConfig, ent *entry.Entry, buildErr bool, parseErr bool, expected time.Time, maxLoss time.Duration) func(*testing.T) {
   391  
   392  	return func(t *testing.T) {
   393  		buildContext := testutil.NewBuildContext(t)
   394  
   395  		gotimeOperator, err := cfg.Build(buildContext)
   396  		if buildErr {
   397  			require.Error(t, err, "expected error when configuring operator")
   398  			return
   399  		}
   400  		require.NoError(t, err)
   401  
   402  		mockOutput := &testutil.Operator{}
   403  		resultChan := make(chan *entry.Entry, 1)
   404  		mockOutput.On("Process", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   405  			resultChan <- args.Get(1).(*entry.Entry)
   406  		}).Return(nil)
   407  
   408  		timeParser := gotimeOperator.(*TimeParserOperator)
   409  		timeParser.OutputOperators = []operator.Operator{mockOutput}
   410  
   411  		err = timeParser.Process(context.Background(), ent)
   412  		if parseErr {
   413  			require.Error(t, err, "expected error when configuring operator")
   414  			return
   415  		}
   416  		require.NoError(t, err)
   417  
   418  		select {
   419  		case e := <-resultChan:
   420  			diff := time.Duration(math.Abs(float64(expected.Sub(e.Timestamp))))
   421  			require.True(t, diff <= maxLoss)
   422  		case <-time.After(time.Second):
   423  			require.FailNow(t, "Timed out waiting for entry to be processed")
   424  		}
   425  	}
   426  }
   427  
   428  func parseTimeTestConfig(layoutType, layout string, parseFrom entry.Field) *TimeParserConfig {
   429  	cfg := NewTimeParserConfig("test_operator_id")
   430  	cfg.OutputIDs = []string{"output1"}
   431  	cfg.TimeParser = helper.TimeParser{
   432  		LayoutType: layoutType,
   433  		Layout:     layout,
   434  		ParseFrom:  &parseFrom,
   435  	}
   436  	return cfg
   437  }