modifying docker --since and --until to support nanoseconds and time zones

Signed-off-by: Mike Brown <brownwm@us.ibm.com>
This commit is contained in:
Mike Brown 2015-10-29 12:51:36 -05:00
parent 55c8a5b21b
commit 1192791154
2 changed files with 169 additions and 32 deletions

View file

@ -1,36 +1,124 @@
package timeutils package timeutils
import ( import (
"fmt"
"math"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
// These are additional predefined layouts for use in Time.Format and Time.Parse
// with --since and --until parameters for `docker logs` and `docker events`
const (
rFC3339Local = "2006-01-02T15:04:05" // RFC3339 with local timezone
rFC3339NanoLocal = "2006-01-02T15:04:05.999999999" // RFC3339Nano with local timezone
dateWithZone = "2006-01-02Z07:00" // RFC3339 with time at 00:00:00
dateLocal = "2006-01-02" // RFC3339 with local timezone and time at 00:00:00
)
// GetTimestamp tries to parse given string as golang duration, // GetTimestamp tries to parse given string as golang duration,
// then RFC3339 time and finally as a Unix timestamp. If // then RFC3339 time and finally as a Unix timestamp. If
// any of these were successful, it returns a Unix timestamp // any of these were successful, it returns a Unix timestamp
// as string otherwise returns the given value back. // as string otherwise returns the given value back.
// In case of duration input, the returned timestamp is computed // In case of duration input, the returned timestamp is computed
// as the given reference time minus the amount of the duration. // as the given reference time minus the amount of the duration.
func GetTimestamp(value string, reference time.Time) string { func GetTimestamp(value string, reference time.Time) (string, error) {
if d, err := time.ParseDuration(value); value != "0" && err == nil { if d, err := time.ParseDuration(value); value != "0" && err == nil {
return strconv.FormatInt(reference.Add(-d).Unix(), 10) return strconv.FormatInt(reference.Add(-d).Unix(), 10), nil
} }
var format string var format string
var parseInLocation bool
// if the string has a Z or a + or three dashes use parse otherwise use parseinlocation
parseInLocation = !(strings.ContainsAny(value, "zZ+") || strings.Count(value, "-") == 3)
if strings.Contains(value, ".") { if strings.Contains(value, ".") {
format = time.RFC3339Nano if parseInLocation {
format = rFC3339NanoLocal
} else { } else {
format = time.RFC3339Nano
}
} else if strings.Contains(value, "T") {
// we want the number of colons in the T portion of the timestamp
tcolons := strings.Count(value, ":")
// if parseInLocation is off and we have a +/- zone offset (not Z) then
// there will be an extra colon in the input for the tz offset subract that
// colon from the tcolons count
if !parseInLocation && !strings.ContainsAny(value, "zZ") && tcolons > 0 {
tcolons--
}
if parseInLocation {
switch tcolons {
case 0:
format = "2006-01-02T15"
case 1:
format = "2006-01-02T15:04"
default:
format = rFC3339Local
}
} else {
switch tcolons {
case 0:
format = "2006-01-02T15Z07:00"
case 1:
format = "2006-01-02T15:04Z07:00"
default:
format = time.RFC3339 format = time.RFC3339
} }
}
} else if parseInLocation {
format = dateLocal
} else {
format = dateWithZone
}
loc := time.FixedZone(time.Now().Zone()) var t time.Time
if len(value) < len(format) { var err error
format = format[:len(value)]
if parseInLocation {
t, err = time.ParseInLocation(format, value, time.FixedZone(time.Now().Zone()))
} else {
t, err = time.Parse(format, value)
} }
t, err := time.ParseInLocation(format, value, loc)
if err != nil { if err != nil {
return value // if there is a `-` then its an RFC3339 like timestamp otherwise assume unixtimestamp
if strings.Contains(value, "-") {
return "", err // was probably an RFC3339 like timestamp but the parser failed with an error
} }
return strconv.FormatInt(t.Unix(), 10) return value, nil // unixtimestamp in and out case (meaning: the value passed at the command line is already in the right format for passing to the server)
}
return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
}
// ParseTimestamps returns seconds and nanoseconds from a timestamp that has the
// format "%d.%09d", time.Unix(), int64(time.Nanosecond()))
// if the incoming nanosecond portion is longer or shorter than 9 digits it is
// converted to nanoseconds. The expectation is that the seconds and
// seconds will be used to create a time variable. For example:
// seconds, nanoseconds, err := ParseTimestamp("1136073600.000000001",0)
// if err == nil since := time.Unix(seconds, nanoseconds)
// returns seconds as def(aultSeconds) if value == ""
func ParseTimestamps(value string, def int64) (int64, int64, error) {
if value == "" {
return def, 0, nil
}
sa := strings.SplitN(value, ".", 2)
s, err := strconv.ParseInt(sa[0], 10, 64)
if err != nil {
return s, 0, err
}
if len(sa) != 2 {
return s, 0, nil
}
n, err := strconv.ParseInt(sa[1], 10, 64)
if err != nil {
return s, n, err
}
// should already be in nanoseconds but just in case convert n to nanoseonds
n = int64(float64(n) * math.Pow(float64(10), float64(9-len(sa[1]))))
return s, n, nil
} }

View file

@ -8,37 +8,86 @@ import (
func TestGetTimestamp(t *testing.T) { func TestGetTimestamp(t *testing.T) {
now := time.Now() now := time.Now()
cases := []struct{ in, expected string }{ cases := []struct {
{"0", "-62167305600"}, // 0 gets parsed year 0 in, expected string
expectedErr bool
}{
// Partial RFC3339 strings get parsed with second precision // Partial RFC3339 strings get parsed with second precision
{"2006-01-02T15:04:05.999999999+07:00", "1136189045"}, {"2006-01-02T15:04:05.999999999+07:00", "1136189045.999999999", false},
{"2006-01-02T15:04:05.999999999Z", "1136214245"}, {"2006-01-02T15:04:05.999999999Z", "1136214245.999999999", false},
{"2006-01-02T15:04:05.999999999", "1136214245"}, {"2006-01-02T15:04:05.999999999", "1136214245.999999999", false},
{"2006-01-02T15:04:05", "1136214245"}, {"2006-01-02T15:04:05Z", "1136214245.000000000", false},
{"2006-01-02T15:04", "1136214240"}, {"2006-01-02T15:04:05", "1136214245.000000000", false},
{"2006-01-02T15", "1136214000"}, {"2006-01-02T15:04:0Z", "", true},
{"2006-01-02T", "1136160000"}, {"2006-01-02T15:04:0", "", true},
{"2006-01-02", "1136160000"}, {"2006-01-02T15:04Z", "1136214240.000000000", false},
{"2006", "1136073600"}, {"2006-01-02T15:04+00:00", "1136214240.000000000", false},
{"2015-05-13T20:39:09Z", "1431549549"}, {"2006-01-02T15:04-00:00", "1136214240.000000000", false},
{"2006-01-02T15:04", "1136214240.000000000", false},
{"2006-01-02T15:0Z", "", true},
{"2006-01-02T15:0", "", true},
{"2006-01-02T15Z", "1136214000.000000000", false},
{"2006-01-02T15+00:00", "1136214000.000000000", false},
{"2006-01-02T15-00:00", "1136214000.000000000", false},
{"2006-01-02T15", "1136214000.000000000", false},
{"2006-01-02T1Z", "1136163600.000000000", false},
{"2006-01-02T1", "1136163600.000000000", false},
{"2006-01-02TZ", "", true},
{"2006-01-02T", "", true},
{"2006-01-02+00:00", "1136160000.000000000", false},
{"2006-01-02-00:00", "1136160000.000000000", false},
{"2006-01-02-00:01", "1136160060.000000000", false},
{"2006-01-02Z", "1136160000.000000000", false},
{"2006-01-02", "1136160000.000000000", false},
{"2015-05-13T20:39:09Z", "1431549549.000000000", false},
// unix timestamps returned as is // unix timestamps returned as is
{"1136073600", "1136073600"}, {"1136073600", "1136073600", false},
{"1136073600.000000001", "1136073600.000000001", false},
// Durations // Durations
{"1m", fmt.Sprintf("%d", now.Add(-1*time.Minute).Unix())}, {"1m", fmt.Sprintf("%d", now.Add(-1*time.Minute).Unix()), false},
{"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix())}, {"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
{"1h30m", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix())}, {"1h30m", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
// String fallback // String fallback
{"invalid", "invalid"}, {"invalid", "invalid", false},
} }
for _, c := range cases { for _, c := range cases {
o := GetTimestamp(c.in, now) o, err := GetTimestamp(c.in, now)
if o != c.expected { if o != c.expected ||
t.Fatalf("wrong value for '%s'. expected:'%s' got:'%s'", c.in, c.expected, o) (err == nil && c.expectedErr) ||
(err != nil && !c.expectedErr) {
t.Errorf("wrong value for '%s'. expected:'%s' got:'%s' with error: `%s`", c.in, c.expected, o, err)
t.Fail()
}
}
}
func TestParseTimestamps(t *testing.T) {
cases := []struct {
in string
def, expectedS, expectedN int64
expectedErr bool
}{
// unix timestamps
{"1136073600", 0, 1136073600, 0, false},
{"1136073600.000000001", 0, 1136073600, 1, false},
{"1136073600.0000000010", 0, 1136073600, 1, false},
{"1136073600.00000001", 0, 1136073600, 10, false},
{"foo.bar", 0, 0, 0, true},
{"1136073600.bar", 0, 1136073600, 0, true},
{"", -1, -1, 0, false},
}
for _, c := range cases {
s, n, err := ParseTimestamps(c.in, c.def)
if s != c.expectedS ||
n != c.expectedN ||
(err == nil && c.expectedErr) ||
(err != nil && !c.expectedErr) {
t.Errorf("wrong values for input `%s` with default `%d` expected:'%d'seconds and `%d`nanosecond got:'%d'seconds and `%d`nanoseconds with error: `%s`", c.in, c.def, c.expectedS, c.expectedN, s, n, err)
t.Fail()
} }
} }
} }