diff --git a/app/vlselect/logsql/logsql.go b/app/vlselect/logsql/logsql.go index fc3f65bba..d6c141ed8 100644 --- a/app/vlselect/logsql/logsql.go +++ b/app/vlselect/logsql/logsql.go @@ -298,11 +298,36 @@ func ProcessQueryRequest(ctx context.Context, w http.ResponseWriter, r *http.Req httpserver.Errorf(w, r, "%s", err) return } - if limit > 0 { - q.AddPipeLimit(uint64(limit)) - } bw := getBufferedWriter(w) + defer func() { + bw.FlushIgnoreErrors() + putBufferedWriter(bw) + }() + w.Header().Set("Content-Type", "application/stream+json") + + if limit > 0 { + if q.CanReturnLastNResults() { + rows, err := getLastNQueryResults(ctx, tenantIDs, q, limit) + if err != nil { + httpserver.Errorf(w, r, "%s", err) + return + } + bb := blockResultPool.Get() + b := bb.B + for i := range rows { + b = logstorage.MarshalFieldsToJSON(b[:0], rows[i].fields) + b = append(b, '\n') + bw.WriteIgnoreErrors(b) + } + bb.B = b + blockResultPool.Put(bb) + return + } + + q.AddPipeLimit(uint64(limit)) + q.Optimize() + } writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) { if len(columns) == 0 || len(columns[0].Values) == 0 { @@ -317,20 +342,103 @@ func ProcessQueryRequest(ctx context.Context, w http.ResponseWriter, r *http.Req blockResultPool.Put(bb) } - w.Header().Set("Content-Type", "application/stream+json") - q.Optimize() - err = vlstorage.RunQuery(ctx, tenantIDs, q, writeBlock) - - bw.FlushIgnoreErrors() - putBufferedWriter(bw) - - if err != nil { + if err := vlstorage.RunQuery(ctx, tenantIDs, q, writeBlock); err != nil { httpserver.Errorf(w, r, "cannot execute query [%s]: %s", q, err) } } var blockResultPool bytesutil.ByteBufferPool +type row struct { + timestamp int64 + fields []logstorage.Field +} + +func getLastNQueryResults(ctx context.Context, tenantIDs []logstorage.TenantID, q *logstorage.Query, limit int) ([]row, error) { + q.AddPipeLimit(uint64(limit + 1)) + q.Optimize() + rows, err := getQueryResultsWithLimit(ctx, tenantIDs, q, limit+1) + if err != nil { + return nil, err + } + if len(rows) <= limit { + // Fast path - the requested time range contains up to limit rows + sortRowsByTime(rows) + return rows, nil + } + + // Slow path - search for the time range with the requested limit rows. + start, end := q.GetFilterTimeRange() + d := (end - start) / 2 + start += d + + qOrig := q + for { + q = qOrig.Clone() + q.AddTimeFilter(start, end) + rows, err := getQueryResultsWithLimit(ctx, tenantIDs, q, limit+1) + if err != nil { + return nil, err + } + + if len(rows) == limit || d == 0 { + sortRowsByTime(rows) + if len(rows) > limit { + rows = rows[:limit] + } + return rows, nil + } + + lastBit := d & 1 + d /= 2 + if len(rows) > limit { + start += d + } else { + start -= d + lastBit + } + } +} + +func sortRowsByTime(rows []row) { + sort.Slice(rows, func(i, j int) bool { + return rows[i].timestamp < rows[j].timestamp + }) +} + +func getQueryResultsWithLimit(ctx context.Context, tenantIDs []logstorage.TenantID, q *logstorage.Query, limit int) ([]row, error) { + ctxWithCancel, cancel := context.WithCancel(ctx) + defer cancel() + + var rows []row + var rowsLock sync.Mutex + writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) { + rowsLock.Lock() + defer rowsLock.Unlock() + + for i, timestamp := range timestamps { + fields := make([]logstorage.Field, len(columns)) + for j := range columns { + f := &fields[j] + f.Name = strings.Clone(columns[j].Name) + f.Value = strings.Clone(columns[j].Values[i]) + } + rows = append(rows, row{ + timestamp: timestamp, + fields: fields, + }) + } + + if len(rows) >= limit { + cancel() + } + } + if err := vlstorage.RunQuery(ctxWithCancel, tenantIDs, q, writeBlock); err != nil { + return nil, err + } + + return rows, nil +} + func parseCommonArgs(r *http.Request) (*logstorage.Query, []logstorage.TenantID, error) { // Extract tenantID tenantID, err := logstorage.GetTenantIDFromRequest(r) @@ -373,10 +481,10 @@ func getTimeNsec(r *http.Request, argName string) (int64, bool, error) { if s == "" { return 0, false, nil } - currentTimestamp := float64(time.Now().UnixNano()) / 1e9 - secs, err := promutils.ParseTimeAt(s, currentTimestamp) + currentTimestamp := time.Now().UnixNano() + nsecs, err := promutils.ParseTimeAt(s, currentTimestamp) if err != nil { return 0, false, fmt.Errorf("cannot parse %s=%s: %w", argName, s, err) } - return int64(secs * 1e9), true, nil + return nsecs, true, nil } diff --git a/docs/VictoriaLogs/CHANGELOG.md b/docs/VictoriaLogs/CHANGELOG.md index 2925cea91..598250081 100644 --- a/docs/VictoriaLogs/CHANGELOG.md +++ b/docs/VictoriaLogs/CHANGELOG.md @@ -19,6 +19,7 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta ## tip +* FEATURE: return the last `N` matching logs from [`/select/logsql/query` HTTP API](https://docs.victoriametrics.com/victorialogs/querying/#querying-logs) with the maximum timestamps if `limit=N` query arg is passed to it. Previously a random subset of matching logs could be returned, which could complicate investigation of the returned logs. * FEATURE: add [`drop_empty_fields` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#drop_empty_fields-pipe) for dropping [log fields](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model) with empty values. ## [v0.15.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.15.0-victorialogs) diff --git a/docs/VictoriaLogs/querying/README.md b/docs/VictoriaLogs/querying/README.md index c115c2360..53581b6b9 100644 --- a/docs/VictoriaLogs/querying/README.md +++ b/docs/VictoriaLogs/querying/README.md @@ -58,12 +58,14 @@ By default the `/select/logsql/query` returns all the log entries matching the g - By closing the response stream at any time. VictoriaLogs stops query execution and frees all the resources occupied by the request as soon as it detects closed client connection. So it is safe running [`*` query](https://docs.victoriametrics.com/victorialogs/logsql/#any-value-filter), which selects all the logs, even if trillions of logs are stored in VictoriaLogs. -- By specifying the maximum number of log entries, which can be returned in the response via `limit` query arg. For example, the following request returns - up to 10 matching log entries: +- By specifying the maximum number of log entries, which can be returned in the response via `limit` query arg. For example, the following command returns + up to 10 most recently added log entries with the `error` [word](https://docs.victoriametrics.com/victorialogs/logsql/#word) + in the [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field): ```sh curl http://localhost:9428/select/logsql/query -d 'query=error' -d 'limit=10' ``` -- By adding [`limit` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#limit-pipe) to the query. For example: +- By adding [`limit` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#limit-pipe) to the query. For example, the following command returns up to 10 **random** log entries + with the `error` [word](https://docs.victoriametrics.com/victorialogs/logsql/#word) in the [`_msg` field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field): ```sh curl http://localhost:9428/select/logsql/query -d 'query=error | limit 10' ``` @@ -87,8 +89,11 @@ This allows post-processing the returned lines at the client side with the usual without worrying about resource usage at VictoriaLogs side. See [these docs](#command-line) for more details. The returned lines aren't sorted by default, since sorting disables the ability to send matching log entries to response stream as soon as they are found. -Query results can be sorted either at VictoriaLogs side via [`sort` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#sort-pipe) -or at client side with the usual `sort` command according to [these docs](#command-line). +Query results can be sorted in the following ways: + +- By passing `limit=N` query arg to `/select/logsql/query`. The up to `N` most recent matching log entries are returned in the response. +- By adding [`sort` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#sort-pipe) to the query. +- By using Unix `sort` command at client side according to [these docs](#command-line). By default the `(AccountID=0, ProjectID=0)` [tenant](https://docs.victoriametrics.com/victorialogs/#multitenancy) is queried. If you need querying other tenant, then specify it via `AccounID` and `ProjectID` http request headers. For example, the following query searches diff --git a/lib/logstorage/logfmt_parser_test.go b/lib/logstorage/logfmt_parser_test.go index 711dd26dd..66a1e6552 100644 --- a/lib/logstorage/logfmt_parser_test.go +++ b/lib/logstorage/logfmt_parser_test.go @@ -12,7 +12,7 @@ func TestLogfmtParser(t *testing.T) { defer putLogfmtParser(p) p.parse(s) - result := marshalFieldsToJSON(nil, p.fields) + result := MarshalFieldsToJSON(nil, p.fields) if string(result) != resultExpected { t.Fatalf("unexpected result when parsing [%s]; got\n%s\nwant\n%s\n", s, result, resultExpected) } diff --git a/lib/logstorage/parser.go b/lib/logstorage/parser.go index 37a962de5..2f58a9111 100644 --- a/lib/logstorage/parser.go +++ b/lib/logstorage/parser.go @@ -279,6 +279,38 @@ func (q *Query) AddCountByTimePipe(step, off int64, fields []string) { } } +// Clone returns a copy of q. +func (q *Query) Clone() *Query { + qStr := q.String() + qCopy, err := ParseQuery(qStr) + if err != nil { + logger.Panicf("BUG: cannot parse %q: %s", qStr, err) + } + return qCopy +} + +// CanReturnLastNResults returns true if time range filter at q can be adjusted for returning the last N results. +func (q *Query) CanReturnLastNResults() bool { + for _, p := range q.pipes { + switch p.(type) { + case *pipeFieldNames, + *pipeFieldValues, + *pipeLimit, + *pipeOffset, + *pipeSort, + *pipeStats, + *pipeUniq: + return false + } + } + return true +} + +// GetFilterTimeRange returns filter time range for the given q. +func (q *Query) GetFilterTimeRange() (int64, int64) { + return getFilterTimeRange(q.f) +} + // AddTimeFilter adds global filter _time:[start ... end] to q. func (q *Query) AddTimeFilter(start, end int64) { startStr := marshalTimestampRFC3339NanoString(nil, start) @@ -1394,12 +1426,12 @@ func parseFilterTime(lex *lexer) (*filterTime, error) { sLower := strings.ToLower(s) if sLower == "now" || startsWithYear(s) { // Parse '_time:YYYY-MM-DD', which transforms to '_time:[YYYY-MM-DD, YYYY-MM-DD+1)' - t, err := promutils.ParseTimeAt(s, float64(lex.currentTimestamp)/1e9) + nsecs, err := promutils.ParseTimeAt(s, lex.currentTimestamp) if err != nil { return nil, fmt.Errorf("cannot parse _time filter: %w", err) } // Round to milliseconds - startTime := int64(math.Round(t*1e3)) * 1e6 + startTime := nsecs endTime := getMatchingEndTime(startTime, s) ft := &filterTime{ minTimestamp: startTime, @@ -1549,12 +1581,11 @@ func parseTime(lex *lexer) (int64, string, error) { if err != nil { return 0, "", err } - t, err := promutils.ParseTimeAt(s, float64(lex.currentTimestamp)/1e9) + nsecs, err := promutils.ParseTimeAt(s, lex.currentTimestamp) if err != nil { return 0, "", err } - // round to milliseconds - return int64(math.Round(t*1e3)) * 1e6, s, nil + return nsecs, s, nil } func quoteStringTokenIfNeeded(s string) string { diff --git a/lib/logstorage/parser_test.go b/lib/logstorage/parser_test.go index 702db4678..b0edc32b7 100644 --- a/lib/logstorage/parser_test.go +++ b/lib/logstorage/parser_test.go @@ -1832,3 +1832,72 @@ func TestQueryGetNeededColumns(t *testing.T) { f(`* | unroll (a, b) | count() r1`, `a,b`, ``) f(`* | unroll if (q:w p:a) (a, b) | count() r1`, `a,b,p,q`, ``) } + +func TestQueryClone(t *testing.T) { + f := func(qStr string) { + t.Helper() + + q, err := ParseQuery(qStr) + if err != nil { + t.Fatalf("cannot parse [%s]: %s", qStr, err) + } + qCopy := q.Clone() + qCopyStr := qCopy.String() + if qStr != qCopyStr { + t.Fatalf("unexpected cloned query\ngot\n%s\nwant\n%s", qCopyStr, qStr) + } + } + + f("*") + f("error") + f("_time:5m error | fields foo, bar") + f("ip:in(foo | fields user_ip) bar | stats by (x:1h, y) count(*) if (user_id:in(q:w | fields abc)) as ccc") +} + +func TestQueryGetFilterTimeRange(t *testing.T) { + f := func(qStr string, startExpected, endExpected int64) { + t.Helper() + + q, err := ParseQuery(qStr) + if err != nil { + t.Fatalf("cannot parse [%s]: %s", qStr, err) + } + start, end := q.GetFilterTimeRange() + if start != startExpected || end != endExpected { + t.Fatalf("unexpected filter time range; got [%d, %d]; want [%d, %d]", start, end, startExpected, endExpected) + } + } + + f("*", -9223372036854775808, 9223372036854775807) + f("_time:2024-05-31T10:20:30.456789123Z", 1717150830456789123, 1717150830456789123) + f("_time:2024-05-31", 1717113600000000000, 1717199999999999999) +} + +func TestQueryCanReturnLastNResults(t *testing.T) { + f := func(qStr string, resultExpected bool) { + t.Helper() + + q, err := ParseQuery(qStr) + if err != nil { + t.Fatalf("cannot parse [%s]: %s", qStr, err) + } + result := q.CanReturnLastNResults() + if result != resultExpected { + t.Fatalf("unexpected result for CanRetrurnLastNResults(%q); got %v; want %v", qStr, result, resultExpected) + } + } + + f("*", true) + f("error", true) + f("error | fields foo | filter foo:bar", true) + f("error | extract 'bar'", true) + f("* | rm x", true) + f("* | stats count() rows", false) + f("* | sort by (x)", false) + f("* | limit 10", false) + f("* | offset 10", false) + f("* | uniq (x)", false) + f("* | field_names", false) + f("* | field_values x", false) + +} diff --git a/lib/logstorage/pipe_pack_json.go b/lib/logstorage/pipe_pack_json.go index af44c8750..0a1686f04 100644 --- a/lib/logstorage/pipe_pack_json.go +++ b/lib/logstorage/pipe_pack_json.go @@ -126,7 +126,7 @@ func (ppp *pipePackJSONProcessor) writeBlock(workerID uint, br *blockResult) { } bufLen := len(buf) - buf = marshalFieldsToJSON(buf, fields) + buf = MarshalFieldsToJSON(buf, fields) v := bytesutil.ToUnsafeString(buf[bufLen:]) shard.rc.addValue(v) } diff --git a/lib/logstorage/rows.go b/lib/logstorage/rows.go index 192205f9c..b9e8df98e 100644 --- a/lib/logstorage/rows.go +++ b/lib/logstorage/rows.go @@ -64,7 +64,8 @@ func (f *Field) marshalToJSON(dst []byte) []byte { return dst } -func marshalFieldsToJSON(dst []byte, fields []Field) []byte { +// MarshalFieldsToJSON appends JSON-marshaled fields to dt and returns the result. +func MarshalFieldsToJSON(dst []byte, fields []Field) []byte { dst = append(dst, '{') if len(fields) > 0 { dst = fields[0].marshalToJSON(dst) diff --git a/lib/logstorage/stats_row_any.go b/lib/logstorage/stats_row_any.go index 6707040cc..0060189ce 100644 --- a/lib/logstorage/stats_row_any.go +++ b/lib/logstorage/stats_row_any.go @@ -99,7 +99,7 @@ func (sap *statsRowAnyProcessor) updateState(br *blockResult, rowIdx int) int { func (sap *statsRowAnyProcessor) finalizeStats() string { bb := bbPool.Get() - bb.B = marshalFieldsToJSON(bb.B, sap.fields) + bb.B = MarshalFieldsToJSON(bb.B, sap.fields) result := string(bb.B) bbPool.Put(bb) diff --git a/lib/logstorage/stats_row_max.go b/lib/logstorage/stats_row_max.go index 8f53f0f65..31c3689a9 100644 --- a/lib/logstorage/stats_row_max.go +++ b/lib/logstorage/stats_row_max.go @@ -206,7 +206,7 @@ func (smp *statsRowMaxProcessor) updateState(v string, br *blockResult, rowIdx i func (smp *statsRowMaxProcessor) finalizeStats() string { bb := bbPool.Get() - bb.B = marshalFieldsToJSON(bb.B, smp.fields) + bb.B = MarshalFieldsToJSON(bb.B, smp.fields) result := string(bb.B) bbPool.Put(bb) diff --git a/lib/logstorage/stats_row_min.go b/lib/logstorage/stats_row_min.go index 9aa69681a..66415dd90 100644 --- a/lib/logstorage/stats_row_min.go +++ b/lib/logstorage/stats_row_min.go @@ -206,7 +206,7 @@ func (smp *statsRowMinProcessor) updateState(v string, br *blockResult, rowIdx i func (smp *statsRowMinProcessor) finalizeStats() string { bb := bbPool.Get() - bb.B = marshalFieldsToJSON(bb.B, smp.fields) + bb.B = MarshalFieldsToJSON(bb.B, smp.fields) result := string(bb.B) bbPool.Put(bb) diff --git a/lib/logstorage/storage_search_test.go b/lib/logstorage/storage_search_test.go index 488c46c48..9f5bde5af 100644 --- a/lib/logstorage/storage_search_test.go +++ b/lib/logstorage/storage_search_test.go @@ -969,7 +969,7 @@ func TestParseStreamFieldsSuccess(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s", err) } - result := marshalFieldsToJSON(nil, labels) + result := MarshalFieldsToJSON(nil, labels) if string(result) != resultExpected { t.Fatalf("unexpected result\ngot\n%s\nwant\n%s", result, resultExpected) } diff --git a/lib/promutils/time.go b/lib/promutils/time.go index 55e9700a0..b2ac05dc0 100644 --- a/lib/promutils/time.go +++ b/lib/promutils/time.go @@ -14,12 +14,12 @@ import ( // // It returns unix timestamp in milliseconds. func ParseTimeMsec(s string) (int64, error) { - currentTimestamp := float64(time.Now().UnixNano()) / 1e9 - secs, err := ParseTimeAt(s, currentTimestamp) + currentTimestamp := time.Now().UnixNano() + nsecs, err := ParseTimeAt(s, currentTimestamp) if err != nil { return 0, err } - msecs := int64(math.Round(secs * 1000)) + msecs := int64(math.Round(float64(nsecs) / 1e6)) return msecs, nil } @@ -33,13 +33,13 @@ const ( // // See https://docs.victoriametrics.com/single-server-victoriametrics/#timestamp-formats // -// It returns unix timestamp in seconds. -func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { +// It returns unix timestamp in nanoseconds. +func ParseTimeAt(s string, currentTimestamp int64) (int64, error) { if s == "now" { return currentTimestamp, nil } sOrig := s - tzOffset := float64(0) + tzOffset := int64(0) if len(sOrig) > 6 { // Try parsing timezone offset tz := sOrig[len(sOrig)-6:] @@ -53,7 +53,7 @@ func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { if err != nil { return 0, fmt.Errorf("cannot parse minute from timezone offset %q: %w", tz, err) } - tzOffset = float64(hour*3600 + minute*60) + tzOffset = int64(hour*3600+minute*60) * 1e9 if isPlus { tzOffset = -tzOffset } @@ -71,7 +71,7 @@ func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { if d > 0 { d = -d } - return currentTimestamp + float64(d)/1e9, nil + return currentTimestamp + int64(d), nil } if len(s) == 4 { // Parse YYYY @@ -83,7 +83,7 @@ func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { if y > maxValidYear || y < minValidYear { return 0, fmt.Errorf("cannot parse year from %q: year must in range [%d, %d]", s, minValidYear, maxValidYear) } - return tzOffset + float64(t.UnixNano())/1e9, nil + return tzOffset + t.UnixNano(), nil } if !strings.Contains(sOrig, "-") { // Parse the timestamp in seconds or in milliseconds @@ -95,7 +95,7 @@ func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { // The timestamp is in milliseconds. Convert it to seconds. ts /= 1000 } - return ts, nil + return int64(math.Round(ts*1e3)) * 1e6, nil } if len(s) == 7 { // Parse YYYY-MM @@ -103,7 +103,7 @@ func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { if err != nil { return 0, err } - return tzOffset + float64(t.UnixNano())/1e9, nil + return tzOffset + t.UnixNano(), nil } if len(s) == 10 { // Parse YYYY-MM-DD @@ -111,7 +111,7 @@ func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { if err != nil { return 0, err } - return tzOffset + float64(t.UnixNano())/1e9, nil + return tzOffset + t.UnixNano(), nil } if len(s) == 13 { // Parse YYYY-MM-DDTHH @@ -119,7 +119,7 @@ func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { if err != nil { return 0, err } - return tzOffset + float64(t.UnixNano())/1e9, nil + return tzOffset + t.UnixNano(), nil } if len(s) == 16 { // Parse YYYY-MM-DDTHH:MM @@ -127,7 +127,7 @@ func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { if err != nil { return 0, err } - return tzOffset + float64(t.UnixNano())/1e9, nil + return tzOffset + t.UnixNano(), nil } if len(s) == 19 { // Parse YYYY-MM-DDTHH:MM:SS @@ -135,12 +135,12 @@ func ParseTimeAt(s string, currentTimestamp float64) (float64, error) { if err != nil { return 0, err } - return tzOffset + float64(t.UnixNano())/1e9, nil + return tzOffset + t.UnixNano(), nil } // Parse RFC3339 t, err := time.Parse(time.RFC3339, sOrig) if err != nil { return 0, err } - return float64(t.UnixNano()) / 1e9, nil + return t.UnixNano(), nil } diff --git a/lib/promutils/time_test.go b/lib/promutils/time_test.go index 9ce9c9a6c..2f9e76b52 100644 --- a/lib/promutils/time_test.go +++ b/lib/promutils/time_test.go @@ -6,7 +6,7 @@ import ( ) func TestParseTimeAtSuccess(t *testing.T) { - f := func(s string, currentTime, resultExpected float64) { + f := func(s string, currentTime, resultExpected int64) { t.Helper() result, err := ParseTimeAt(s, currentTime) if err != nil { @@ -17,65 +17,65 @@ func TestParseTimeAtSuccess(t *testing.T) { } } - now := float64(time.Now().UnixNano()) / 1e9 + now := time.Now().UnixNano() // unix timestamp in seconds - f("1562529662", now, 1562529662) - f("1562529662.678", now, 1562529662.678) + f("1562529662", now, 1562529662*1e9) + f("1562529662.678", now, 1562529662678*1e6) // unix timestamp in milliseconds - f("1562529662678", now, 1562529662.678) + f("1562529662678", now, 1562529662678*1e6) // duration relative to the current time f("now", now, now) - f("1h5s", now, now-3605) + f("1h5s", now, now-3605*1e9) // negative duration relative to the current time - f("-5m", now, now-5*60) - f("-123", now, now-123) - f("-123.456", now, now-123.456) - f("now-1h5m", now, now-(3600+5*60)) + f("-5m", now, now-5*60*1e9) + f("-123", now, now-123*1e9) + f("-123.456", now, now-123456*1e6) + f("now-1h5m", now, now-(3600+5*60)*1e9) // Year - f("2023", now, 1.6725312e+09) - f("2023Z", now, 1.6725312e+09) - f("2023+02:00", now, 1.672524e+09) - f("2023-02:00", now, 1.6725384e+09) + f("2023", now, 1.6725312e+09*1e9) + f("2023Z", now, 1.6725312e+09*1e9) + f("2023+02:00", now, 1.672524e+09*1e9) + f("2023-02:00", now, 1.6725384e+09*1e9) // Year and month - f("2023-05", now, 1.6828992e+09) - f("2023-05Z", now, 1.6828992e+09) - f("2023-05+02:00", now, 1.682892e+09) - f("2023-05-02:00", now, 1.6829064e+09) + f("2023-05", now, 1.6828992e+09*1e9) + f("2023-05Z", now, 1.6828992e+09*1e9) + f("2023-05+02:00", now, 1.682892e+09*1e9) + f("2023-05-02:00", now, 1.6829064e+09*1e9) // Year, month and day - f("2023-05-20", now, 1.6845408e+09) - f("2023-05-20Z", now, 1.6845408e+09) - f("2023-05-20+02:30", now, 1.6845318e+09) - f("2023-05-20-02:30", now, 1.6845498e+09) + f("2023-05-20", now, 1.6845408e+09*1e9) + f("2023-05-20Z", now, 1.6845408e+09*1e9) + f("2023-05-20+02:30", now, 1.6845318e+09*1e9) + f("2023-05-20-02:30", now, 1.6845498e+09*1e9) // Year, month, day and hour - f("2023-05-20T04", now, 1.6845552e+09) - f("2023-05-20T04Z", now, 1.6845552e+09) - f("2023-05-20T04+02:30", now, 1.6845462e+09) - f("2023-05-20T04-02:30", now, 1.6845642e+09) + f("2023-05-20T04", now, 1.6845552e+09*1e9) + f("2023-05-20T04Z", now, 1.6845552e+09*1e9) + f("2023-05-20T04+02:30", now, 1.6845462e+09*1e9) + f("2023-05-20T04-02:30", now, 1.6845642e+09*1e9) // Year, month, day, hour and minute - f("2023-05-20T04:57", now, 1.68455862e+09) - f("2023-05-20T04:57Z", now, 1.68455862e+09) - f("2023-05-20T04:57+02:30", now, 1.68454962e+09) - f("2023-05-20T04:57-02:30", now, 1.68456762e+09) + f("2023-05-20T04:57", now, 1.68455862e+09*1e9) + f("2023-05-20T04:57Z", now, 1.68455862e+09*1e9) + f("2023-05-20T04:57+02:30", now, 1.68454962e+09*1e9) + f("2023-05-20T04:57-02:30", now, 1.68456762e+09*1e9) // Year, month, day, hour, minute and second - f("2023-05-20T04:57:43", now, 1.684558663e+09) - f("2023-05-20T04:57:43Z", now, 1.684558663e+09) - f("2023-05-20T04:57:43+02:30", now, 1.684549663e+09) - f("2023-05-20T04:57:43-02:30", now, 1.684567663e+09) + f("2023-05-20T04:57:43", now, 1.684558663e+09*1e9) + f("2023-05-20T04:57:43Z", now, 1.684558663e+09*1e9) + f("2023-05-20T04:57:43+02:30", now, 1.684549663e+09*1e9) + f("2023-05-20T04:57:43-02:30", now, 1.684567663e+09*1e9) // milliseconds - f("2023-05-20T04:57:43.123Z", now, 1.6845586631230001e+09) - f("2023-05-20T04:57:43.123456789+02:30", now, 1.6845496631234567e+09) - f("2023-05-20T04:57:43.123456789-02:30", now, 1.6845676631234567e+09) + f("2023-05-20T04:57:43.123Z", now, 1684558663123000000) + f("2023-05-20T04:57:43.123456789+02:30", now, 1684549663123456789) + f("2023-05-20T04:57:43.123456789-02:30", now, 1684567663123456789) } func TestParseTimeMsecFailure(t *testing.T) {