| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 1 | // Copyright 2013 The Prometheus Authors |
| 2 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 3 | // you may not use this file except in compliance with the License. |
| 4 | // You may obtain a copy of the License at |
| 5 | // |
| 6 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 7 | // |
| 8 | // Unless required by applicable law or agreed to in writing, software |
| 9 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 11 | // See the License for the specific language governing permissions and |
| 12 | // limitations under the License. |
| 13 | |
| 14 | package model |
| 15 | |
| 16 | import ( |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 17 | "encoding/json" |
| 18 | "errors" |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 19 | "fmt" |
| 20 | "math" |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 21 | "strconv" |
| 22 | "strings" |
| 23 | "time" |
| 24 | ) |
| 25 | |
| 26 | const ( |
| 27 | // MinimumTick is the minimum supported time resolution. This has to be |
| 28 | // at least time.Second in order for the code below to work. |
| 29 | minimumTick = time.Millisecond |
| 30 | // second is the Time duration equivalent to one second. |
| 31 | second = int64(time.Second / minimumTick) |
| 32 | // The number of nanoseconds per minimum tick. |
| 33 | nanosPerTick = int64(minimumTick / time.Nanosecond) |
| 34 | |
| 35 | // Earliest is the earliest Time representable. Handy for |
| 36 | // initializing a high watermark. |
| 37 | Earliest = Time(math.MinInt64) |
| 38 | // Latest is the latest Time representable. Handy for initializing |
| 39 | // a low watermark. |
| 40 | Latest = Time(math.MaxInt64) |
| 41 | ) |
| 42 | |
| 43 | // Time is the number of milliseconds since the epoch |
| 44 | // (1970-01-01 00:00 UTC) excluding leap seconds. |
| 45 | type Time int64 |
| 46 | |
| 47 | // Interval describes an interval between two timestamps. |
| 48 | type Interval struct { |
| 49 | Start, End Time |
| 50 | } |
| 51 | |
| 52 | // Now returns the current time as a Time. |
| 53 | func Now() Time { |
| 54 | return TimeFromUnixNano(time.Now().UnixNano()) |
| 55 | } |
| 56 | |
| 57 | // TimeFromUnix returns the Time equivalent to the Unix Time t |
| 58 | // provided in seconds. |
| 59 | func TimeFromUnix(t int64) Time { |
| 60 | return Time(t * second) |
| 61 | } |
| 62 | |
| 63 | // TimeFromUnixNano returns the Time equivalent to the Unix Time |
| 64 | // t provided in nanoseconds. |
| 65 | func TimeFromUnixNano(t int64) Time { |
| 66 | return Time(t / nanosPerTick) |
| 67 | } |
| 68 | |
| 69 | // Equal reports whether two Times represent the same instant. |
| 70 | func (t Time) Equal(o Time) bool { |
| 71 | return t == o |
| 72 | } |
| 73 | |
| 74 | // Before reports whether the Time t is before o. |
| 75 | func (t Time) Before(o Time) bool { |
| 76 | return t < o |
| 77 | } |
| 78 | |
| 79 | // After reports whether the Time t is after o. |
| 80 | func (t Time) After(o Time) bool { |
| 81 | return t > o |
| 82 | } |
| 83 | |
| 84 | // Add returns the Time t + d. |
| 85 | func (t Time) Add(d time.Duration) Time { |
| 86 | return t + Time(d/minimumTick) |
| 87 | } |
| 88 | |
| 89 | // Sub returns the Duration t - o. |
| 90 | func (t Time) Sub(o Time) time.Duration { |
| 91 | return time.Duration(t-o) * minimumTick |
| 92 | } |
| 93 | |
| 94 | // Time returns the time.Time representation of t. |
| 95 | func (t Time) Time() time.Time { |
| 96 | return time.Unix(int64(t)/second, (int64(t)%second)*nanosPerTick) |
| 97 | } |
| 98 | |
| 99 | // Unix returns t as a Unix time, the number of seconds elapsed |
| 100 | // since January 1, 1970 UTC. |
| 101 | func (t Time) Unix() int64 { |
| 102 | return int64(t) / second |
| 103 | } |
| 104 | |
| 105 | // UnixNano returns t as a Unix time, the number of nanoseconds elapsed |
| 106 | // since January 1, 1970 UTC. |
| 107 | func (t Time) UnixNano() int64 { |
| 108 | return int64(t) * nanosPerTick |
| 109 | } |
| 110 | |
| 111 | // The number of digits after the dot. |
| 112 | var dotPrecision = int(math.Log10(float64(second))) |
| 113 | |
| 114 | // String returns a string representation of the Time. |
| 115 | func (t Time) String() string { |
| 116 | return strconv.FormatFloat(float64(t)/float64(second), 'f', -1, 64) |
| 117 | } |
| 118 | |
| 119 | // MarshalJSON implements the json.Marshaler interface. |
| 120 | func (t Time) MarshalJSON() ([]byte, error) { |
| 121 | return []byte(t.String()), nil |
| 122 | } |
| 123 | |
| 124 | // UnmarshalJSON implements the json.Unmarshaler interface. |
| 125 | func (t *Time) UnmarshalJSON(b []byte) error { |
| 126 | p := strings.Split(string(b), ".") |
| 127 | switch len(p) { |
| 128 | case 1: |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 129 | v, err := strconv.ParseInt(p[0], 10, 64) |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 130 | if err != nil { |
| 131 | return err |
| 132 | } |
| 133 | *t = Time(v * second) |
| 134 | |
| 135 | case 2: |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 136 | v, err := strconv.ParseInt(p[0], 10, 64) |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 137 | if err != nil { |
| 138 | return err |
| 139 | } |
| 140 | v *= second |
| 141 | |
| 142 | prec := dotPrecision - len(p[1]) |
| 143 | if prec < 0 { |
| 144 | p[1] = p[1][:dotPrecision] |
| 145 | } else if prec > 0 { |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 146 | p[1] += strings.Repeat("0", prec) |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 147 | } |
| 148 | |
| 149 | va, err := strconv.ParseInt(p[1], 10, 32) |
| 150 | if err != nil { |
| 151 | return err |
| 152 | } |
| 153 | |
| 154 | // If the value was something like -0.1 the negative is lost in the |
| 155 | // parsing because of the leading zero, this ensures that we capture it. |
| 156 | if len(p[0]) > 0 && p[0][0] == '-' && v+va > 0 { |
| 157 | *t = Time(v+va) * -1 |
| 158 | } else { |
| 159 | *t = Time(v + va) |
| 160 | } |
| 161 | |
| 162 | default: |
| 163 | return fmt.Errorf("invalid time %q", string(b)) |
| 164 | } |
| 165 | return nil |
| 166 | } |
| 167 | |
| 168 | // Duration wraps time.Duration. It is used to parse the custom duration format |
| 169 | // from YAML. |
| 170 | // This type should not propagate beyond the scope of input/output processing. |
| 171 | type Duration time.Duration |
| 172 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 173 | // Set implements pflag/flag.Value. |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 174 | func (d *Duration) Set(s string) error { |
| 175 | var err error |
| 176 | *d, err = ParseDuration(s) |
| 177 | return err |
| 178 | } |
| 179 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 180 | // Type implements pflag.Value. |
| 181 | func (*Duration) Type() string { |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 182 | return "duration" |
| 183 | } |
| 184 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 185 | func isdigit(c byte) bool { return c >= '0' && c <= '9' } |
| 186 | |
| 187 | // Units are required to go in order from biggest to smallest. |
| 188 | // This guards against confusion from "1m1d" being 1 minute + 1 day, not 1 month + 1 day. |
| 189 | var unitMap = map[string]struct { |
| 190 | pos int |
| 191 | mult uint64 |
| 192 | }{ |
| 193 | "ms": {7, uint64(time.Millisecond)}, |
| 194 | "s": {6, uint64(time.Second)}, |
| 195 | "m": {5, uint64(time.Minute)}, |
| 196 | "h": {4, uint64(time.Hour)}, |
| 197 | "d": {3, uint64(24 * time.Hour)}, |
| 198 | "w": {2, uint64(7 * 24 * time.Hour)}, |
| 199 | "y": {1, uint64(365 * 24 * time.Hour)}, |
| 200 | } |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 201 | |
| 202 | // ParseDuration parses a string into a time.Duration, assuming that a year |
| 203 | // always has 365d, a week always has 7d, and a day always has 24h. |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 204 | // Negative durations are not supported. |
| 205 | func ParseDuration(s string) (Duration, error) { |
| 206 | switch s { |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 207 | case "0": |
| 208 | // Allow 0 without a unit. |
| 209 | return 0, nil |
| 210 | case "": |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 211 | return 0, errors.New("empty duration string") |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 212 | } |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 213 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 214 | orig := s |
| 215 | var dur uint64 |
| 216 | lastUnitPos := 0 |
| 217 | |
| 218 | for s != "" { |
| 219 | if !isdigit(s[0]) { |
| 220 | return 0, fmt.Errorf("not a valid duration string: %q", orig) |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 221 | } |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 222 | // Consume [0-9]* |
| 223 | i := 0 |
| 224 | for ; i < len(s) && isdigit(s[i]); i++ { |
| 225 | } |
| 226 | v, err := strconv.ParseUint(s[:i], 10, 0) |
| 227 | if err != nil { |
| 228 | return 0, fmt.Errorf("not a valid duration string: %q", orig) |
| 229 | } |
| 230 | s = s[i:] |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 231 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 232 | // Consume unit. |
| 233 | for i = 0; i < len(s) && !isdigit(s[i]); i++ { |
| 234 | } |
| 235 | if i == 0 { |
| 236 | return 0, fmt.Errorf("not a valid duration string: %q", orig) |
| 237 | } |
| 238 | u := s[:i] |
| 239 | s = s[i:] |
| 240 | unit, ok := unitMap[u] |
| 241 | if !ok { |
| 242 | return 0, fmt.Errorf("unknown unit %q in duration %q", u, orig) |
| 243 | } |
| 244 | if unit.pos <= lastUnitPos { // Units must go in order from biggest to smallest. |
| 245 | return 0, fmt.Errorf("not a valid duration string: %q", orig) |
| 246 | } |
| 247 | lastUnitPos = unit.pos |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 248 | // Check if the provided duration overflows time.Duration (> ~ 290years). |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 249 | if v > 1<<63/unit.mult { |
| 250 | return 0, errors.New("duration out of range") |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 251 | } |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 252 | dur += v * unit.mult |
| 253 | if dur > 1<<63-1 { |
| 254 | return 0, errors.New("duration out of range") |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 255 | } |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 256 | } |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 257 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 258 | return Duration(dur), nil |
| 259 | } |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 260 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 261 | // ParseDurationAllowNegative is like ParseDuration but also accepts negative durations. |
| 262 | func ParseDurationAllowNegative(s string) (Duration, error) { |
| 263 | if s == "" || s[0] != '-' { |
| 264 | return ParseDuration(s) |
| 265 | } |
| 266 | |
| 267 | d, err := ParseDuration(s[1:]) |
| 268 | |
| 269 | return -d, err |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 270 | } |
| 271 | |
| 272 | func (d Duration) String() string { |
| 273 | var ( |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 274 | ms = int64(time.Duration(d) / time.Millisecond) |
| 275 | r = "" |
| 276 | sign = "" |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 277 | ) |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 278 | |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 279 | if ms == 0 { |
| 280 | return "0s" |
| 281 | } |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 282 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 283 | if ms < 0 { |
| 284 | sign, ms = "-", -ms |
| 285 | } |
| 286 | |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 287 | f := func(unit string, mult int64, exact bool) { |
| 288 | if exact && ms%mult != 0 { |
| 289 | return |
| 290 | } |
| 291 | if v := ms / mult; v > 0 { |
| 292 | r += fmt.Sprintf("%d%s", v, unit) |
| 293 | ms -= v * mult |
| 294 | } |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 295 | } |
| 296 | |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 297 | // Only format years and weeks if the remainder is zero, as it is often |
| 298 | // easier to read 90d than 12w6d. |
| 299 | f("y", 1000*60*60*24*365, true) |
| 300 | f("w", 1000*60*60*24*7, true) |
| 301 | |
| 302 | f("d", 1000*60*60*24, false) |
| 303 | f("h", 1000*60*60, false) |
| 304 | f("m", 1000*60, false) |
| 305 | f("s", 1000, false) |
| 306 | f("ms", 1, false) |
| 307 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 308 | return sign + r |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 309 | } |
| 310 | |
| 311 | // MarshalJSON implements the json.Marshaler interface. |
| 312 | func (d Duration) MarshalJSON() ([]byte, error) { |
| 313 | return json.Marshal(d.String()) |
| 314 | } |
| 315 | |
| 316 | // UnmarshalJSON implements the json.Unmarshaler interface. |
| 317 | func (d *Duration) UnmarshalJSON(bytes []byte) error { |
| 318 | var s string |
| 319 | if err := json.Unmarshal(bytes, &s); err != nil { |
| 320 | return err |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 321 | } |
| khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 322 | dur, err := ParseDuration(s) |
| 323 | if err != nil { |
| 324 | return err |
| 325 | } |
| 326 | *d = dur |
| 327 | return nil |
| 328 | } |
| 329 | |
| 330 | // MarshalText implements the encoding.TextMarshaler interface. |
| 331 | func (d *Duration) MarshalText() ([]byte, error) { |
| 332 | return []byte(d.String()), nil |
| 333 | } |
| 334 | |
| 335 | // UnmarshalText implements the encoding.TextUnmarshaler interface. |
| 336 | func (d *Duration) UnmarshalText(text []byte) error { |
| 337 | var err error |
| 338 | *d, err = ParseDuration(string(text)) |
| 339 | return err |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 340 | } |
| 341 | |
| 342 | // MarshalYAML implements the yaml.Marshaler interface. |
| 343 | func (d Duration) MarshalYAML() (interface{}, error) { |
| 344 | return d.String(), nil |
| 345 | } |
| 346 | |
| 347 | // UnmarshalYAML implements the yaml.Unmarshaler interface. |
| 348 | func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { |
| 349 | var s string |
| 350 | if err := unmarshal(&s); err != nil { |
| 351 | return err |
| 352 | } |
| 353 | dur, err := ParseDuration(s) |
| 354 | if err != nil { |
| 355 | return err |
| 356 | } |
| 357 | *d = dur |
| 358 | return nil |
| 359 | } |