query/common/time_filter.go (326 lines of code) (raw):
// Copyright (c) 2017-2018 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"github.com/uber/aresdb/query/expr"
"github.com/uber/aresdb/utils"
"strconv"
"strings"
"time"
)
var timeUnitMap = map[string]string{
"year": "y",
"quarter": "q",
"month": "M",
"week": "w",
"day": "d",
"hour": "h",
"quarter-hour": "15m",
"minute": "m",
"second": "s",
}
// AlignedTime is time that is calendar aligned to the unit.
type AlignedTime struct {
Time time.Time `json:"time"`
// Values for unit: y, q, M, w, d, {12, 8, 6, 4, 3, 2}h, h, {30, 20, 15, 12, 10, 6, 5, 4, 3, 2}m, m
Unit string `json:"unit"`
}
// adjustMidnight adjusts daylight saving anomalies in a few timezones
// that return day boundary/midnight as either 23:00 (of the previous day) or 01:00.
//
// Fix for America/Sao_Paulo daylight saving starts (2016-10-16):
// The midnight of 2016-10-16 does not exist and time.Date returns 23:00 of the previous day.
//
// Must check whether the 1 hour rewind still gives the same day:
// For Asia/Beirut, this is not true for 2017-03-26, and true for 2017-03-27 and beyond.
func adjustMidnight(t time.Time) time.Time {
if t.Hour() == 23 {
// Add one hour from 23:00 to 01:00 on the transition day;
// and from 23:00 to 00:00 on non-transition days.
return t.Add(time.Hour)
} else if t.Hour() == 1 {
t2 := t.Add(-time.Hour)
if t2.Day() == t.Day() {
// Must check whether the 1 hour rewind still gives the same day:
// For Asia/Beirut, this is false for 2017-03-26 (transition day), and true for 2017-03-27.
return t2
}
}
return t
}
// ParseTimezone parses timezone
func ParseTimezone(timezone string) (*time.Location, error) {
segments := strings.Split(timezone, ":")
hours, err := strconv.Atoi(segments[0])
if err == nil {
minutes := 0
if len(segments) > 1 {
minutes, err = strconv.Atoi(segments[1])
}
if err == nil {
if hours < 0 {
minutes = -minutes
}
return time.FixedZone(timezone, hours*60*60+minutes*60), nil
}
}
return time.LoadLocation(timezone)
}
// GetCurrentCalendarUnit returns the start and end of the calendar unit for base.
func GetCurrentCalendarUnit(base time.Time, unit string) (start, end time.Time, err error) {
return applyTimeOffset(base, 0, unit)
}
// Returns the start and end of the calendar `unit` that is `amount` `unit`s later from `base`.
func applyTimeOffset(base time.Time, amount int, unit string) (start, end time.Time, err error) {
monthStart := time.Date(base.Year(), base.Month(), 1, 0, 0, 0, 0, base.Location())
monthStart = adjustMidnight(monthStart)
dayStart := time.Date(base.Year(), base.Month(), base.Day(), 0, 0, 0, 0, base.Location())
dayStart = adjustMidnight(dayStart)
switch unit {
case "y":
start = time.Date(base.Year()+amount, time.January, 1, 0, 0, 0, 0, base.Location())
end = time.Date(base.Year()+1+amount, time.January, 1, 0, 0, 0, 0, base.Location())
start = adjustMidnight(start)
end = adjustMidnight(end)
case "q":
start = monthStart.AddDate(0, (1-int(base.Month()))%3+3*amount, 0)
end = start.AddDate(0, 3, 0)
start = adjustMidnight(start)
end = adjustMidnight(end)
case "M":
start = monthStart.AddDate(0, amount, 0)
end = start.AddDate(0, 1, 0)
start = adjustMidnight(start)
end = adjustMidnight(end)
case "w":
start = dayStart.AddDate(0, 0, (-int(base.Weekday())-6)%7+7*amount)
end = start.AddDate(0, 0, 7)
start = adjustMidnight(start)
end = adjustMidnight(end)
case "d":
start = dayStart.AddDate(0, 0, amount)
end = start.AddDate(0, 0, 1)
start = adjustMidnight(start)
end = adjustMidnight(end)
case "h":
// Round to hour.
base = time.Date(base.Year(), base.Month(), base.Day(), base.Hour(), 0, 0, 0, base.Location())
// Apply the offset.
start = base.Add(time.Duration(amount) * time.Hour)
end = start.Add(time.Hour)
case "15m":
// Round to quarter-hour.
base = time.Date(base.Year(), base.Month(), base.Day(), base.Hour(), base.Minute()-base.Minute()%15, 0, 0, base.Location())
// Apply the offset.
start = base.Add(time.Duration(amount) * time.Minute * 15)
end = start.Add(time.Minute * 15)
case "m":
// Round to minute.
base = time.Date(base.Year(), base.Month(), base.Day(), base.Hour(), base.Minute(), 0, 0, base.Location())
// Apply the offset.
start = base.Add(time.Duration(amount) * time.Minute)
end = start.Add(time.Minute)
default:
err = utils.StackError(nil, "Unknown time filter unit: %s", unit)
}
return
}
// Returns the start and end of the absolute calendar unit specified in `dateExpr` and `timeExpr`.
func parseAbsoluteTime(dateExpr, timeExpr string, location *time.Location) (start, end time.Time, unit string, err error) {
var year, quarter, hour, minute int
month, day := time.January, 1
segments := strings.Split(dateExpr, "-")
if len(segments) > 3 {
err = utils.StackError(nil, "Unknown time expression: %s %s", dateExpr, timeExpr)
return
}
year, err = strconv.Atoi(segments[0])
if err != nil {
err = utils.StackError(err, "failed to parse %s as year", segments[0])
return
}
unit = "y"
if len(segments) >= 2 {
if segments[1][0] == 'Q' {
quarter, err = strconv.Atoi(segments[1][1:])
if err != nil {
err = utils.StackError(err, "failed to parse %s as quarter", segments[1][1:])
return
}
if len(segments) == 3 {
err = utils.StackError(nil, "Unknown time expression: %s %s", dateExpr, timeExpr)
return
}
month = time.January + time.Month(quarter-1)*3
unit = "q"
} else {
var monthNumber int
monthNumber, err = strconv.Atoi(segments[1])
if err != nil {
err = utils.StackError(err, "failed to parse %s as month", segments[1])
return
}
month = time.Month(monthNumber)
unit = "M"
}
}
if len(segments) == 3 {
day, err = strconv.Atoi(segments[2])
if err != nil {
err = utils.StackError(err, "failed to parse %s as day", segments[2])
return
}
unit = "d"
} else if timeExpr != "" {
err = utils.StackError(nil, "Unknown time expression: %s %s", dateExpr, timeExpr)
return
}
if timeExpr != "" {
segments = strings.Split(timeExpr, ":")
if len(segments) > 2 {
err = utils.StackError(nil, "Unknown time expression: %s %s", dateExpr, timeExpr)
return
}
hour, err = strconv.Atoi(segments[0])
if err != nil {
err = utils.StackError(err, "failed to parse %s as hour", segments[0])
return
}
unit = "h"
if len(segments) == 2 {
minute, err = strconv.Atoi(segments[1])
if err != nil {
err = utils.StackError(err, "failed to parse %s as minute", segments[1])
return
}
unit = "m"
// Temporary hack until summary switch to use relative time expression.
if minute%15 == 0 {
unit = "15m"
}
}
}
t := time.Date(year, month, day, hour, minute, 0, 0, location)
if hour == 0 {
t = adjustMidnight(t)
}
start, end, err = applyTimeOffset(t, 0, unit)
return
}
// Returns the start and end of the calendar unit specified in `expression`.
func parseTimeFilterExpression(expression string, now time.Time) (start, end time.Time, unit string, err error) {
start, end = now, now
unit = "m"
if expression == "now" {
unit = "s"
return
}
if expression == "today" {
expression = "this day"
} else if expression == "yesterday" {
expression = "last day"
}
var amount int
segments := strings.Split(expression, " ")
if segments[0] == "this" {
if len(segments) != 2 {
err = utils.StackError(nil, "Unknown time filter expression: %s", expression)
return
}
unit = timeUnitMap[segments[1]]
if unit == "" {
err = utils.StackError(nil, "Unknown time filter unit: %s", segments[1])
}
start, end, err = applyTimeOffset(now, 0, unit)
return
} else if segments[0] == "last" {
if len(segments) != 2 {
err = utils.StackError(nil, "Unknown time filter expression: %s", expression)
return
}
unit = timeUnitMap[segments[1]]
if unit == "" {
err = utils.StackError(nil, "Unknown time filter unit: %s", segments[1])
}
start, end, err = applyTimeOffset(now, -1, unit)
return
} else if segments[len(segments)-1] == "ago" {
if len(segments) != 3 {
err = utils.StackError(nil, "Unknown time filter expression: %s", expression)
return
}
amount, err = strconv.Atoi(segments[0])
if err != nil {
err = utils.StackError(err, "failed to parse %s as a number", segments[0])
return
}
unit = timeUnitMap[segments[1][:len(segments[1])-1]]
if unit == "" {
err = utils.StackError(nil, "Unknown time filter unit: %s", segments[1])
}
start, end, err = applyTimeOffset(now, -amount, unit)
return
} else if len(segments) == 1 {
amount, err = strconv.Atoi(expression[:len(expression)-1])
if err == nil {
unit = expression[len(expression)-1:]
start, end, err = applyTimeOffset(now, amount, unit)
if err == nil {
return
}
}
}
dateExpr := segments[0]
var timeExpr string
if len(segments) == 2 {
timeExpr = segments[1]
} else if len(segments) > 2 {
err = utils.StackError(nil, "Unknown time filter expression: %s", expression)
return
} else if len(segments) == 1 {
var seconds int64
seconds, err = strconv.ParseInt(segments[0], 10, 64)
if seconds > 99999999999 {
//we will assume data over 99999999999 will be timestamp in ms, and convert it to be in seconds
seconds = seconds / 1000
}
// Numbers above 9999999 are treated as timestamps, otherwise the corresponding Time object (of year 10000 and beyond)
// will fail JSON marshaling, and criples debugz.
if err == nil && seconds > 9999999 {
t := time.Unix(seconds, 0).In(now.Location())
rounded := t.Round(time.Minute)
if rounded.Equal(t) {
start = rounded
end = rounded
unit = "m"
} else {
start, end = t, t
unit = "s"
}
return
}
}
start, end, unit, err = parseAbsoluteTime(dateExpr, timeExpr, now.Location())
return
}
// ParseTimeFilter parses time filter
func ParseTimeFilter(filter TimeFilter, loc *time.Location, now time.Time) (from, to *AlignedTime, err error) {
if loc == nil {
loc = time.UTC
}
now = now.In(loc).Round(time.Second)
if filter.From != "" {
from = &AlignedTime{}
from.Time, _, from.Unit, err = parseTimeFilterExpression(filter.From, now)
if err != nil {
err = utils.StackError(err, "failed to parse time filter `from` expression: %s", filter.From)
return
}
}
if filter.To != "" {
to = &AlignedTime{}
_, to.Time, to.Unit, err = parseTimeFilterExpression(filter.To, now)
if err != nil {
err = utils.StackError(err, "failed to parse time filter `to` expression: %s", filter.To)
return
}
} else if from != nil {
// Populate to with now if from is present.
to = &AlignedTime{Time: now, Unit: "s"}
}
return
}
// CreateTimeFilterExpr creates time filter expr
func CreateTimeFilterExpr(expression expr.Expr, from, to *AlignedTime) (fromExpr, toExpr expr.Expr) {
if from != nil && from.Unit != "" {
fromExpr = &expr.BinaryExpr{
ExprType: expr.Boolean,
Op: expr.GTE,
LHS: expression,
RHS: &expr.NumberLiteral{
Int: int(from.Time.Unix()),
Expr: strconv.FormatInt(from.Time.Unix(), 10),
ExprType: expr.Unsigned,
},
}
}
if to != nil && to.Unit != "" {
toExpr = &expr.BinaryExpr{
ExprType: expr.Boolean,
Op: expr.LT,
LHS: expression,
RHS: &expr.NumberLiteral{
Int: int(to.Time.Unix()),
Expr: strconv.FormatInt(to.Time.Unix(), 10),
ExprType: expr.Unsigned,
},
}
}
return
}