window.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. package kubecost
  2. import (
  3. "bytes"
  4. "fmt"
  5. "github.com/opencost/opencost/pkg/log"
  6. "math"
  7. "regexp"
  8. "strconv"
  9. "time"
  10. "github.com/opencost/opencost/pkg/util/timeutil"
  11. "github.com/opencost/opencost/pkg/env"
  12. "github.com/opencost/opencost/pkg/thanos"
  13. )
  14. const (
  15. minutesPerDay = 60 * 24
  16. minutesPerHour = 60
  17. hoursPerDay = 24
  18. )
  19. var (
  20. durationRegex = regexp.MustCompile(`^(\d+)(m|h|d)$`)
  21. durationOffsetRegex = regexp.MustCompile(`^(\d+)(m|h|d) offset (\d+)(m|h|d)$`)
  22. offesetRegex = regexp.MustCompile(`^(\+|-)(\d\d):(\d\d)$`)
  23. rfc3339 = `\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ`
  24. rfcRegex = regexp.MustCompile(fmt.Sprintf(`(%s),(%s)`, rfc3339, rfc3339))
  25. timestampPairRegex = regexp.MustCompile(`^(\d+)[,|-](\d+)$`)
  26. )
  27. // RoundBack rounds the given time back to a multiple of the given resolution
  28. // in the given time's timezone.
  29. // e.g. 2020-01-01T12:37:48-0700, 24h = 2020-01-01T00:00:00-0700
  30. func RoundBack(t time.Time, resolution time.Duration) time.Time {
  31. _, offSec := t.Zone()
  32. return t.Add(time.Duration(offSec) * time.Second).Truncate(resolution).Add(-time.Duration(offSec) * time.Second)
  33. }
  34. // RoundForward rounds the given time forward to a multiple of the given resolution
  35. // in the given time's timezone.
  36. // e.g. 2020-01-01T12:37:48-0700, 24h = 2020-01-02T00:00:00-0700
  37. func RoundForward(t time.Time, resolution time.Duration) time.Time {
  38. back := RoundBack(t, resolution)
  39. if back.Equal(t) {
  40. // The given time is exactly a multiple of the given resolution
  41. return t
  42. }
  43. return back.Add(resolution)
  44. }
  45. // Window defines a period of time with a start and an end. If either start or
  46. // end are nil it indicates an open time period.
  47. type Window struct {
  48. start *time.Time
  49. end *time.Time
  50. }
  51. // NewWindow creates and returns a new Window instance from the given times
  52. func NewWindow(start, end *time.Time) Window {
  53. return Window{
  54. start: start,
  55. end: end,
  56. }
  57. }
  58. // NewClosedWindow creates and returns a new Window instance from the given
  59. // times, which cannot be nil, so they are value types.
  60. func NewClosedWindow(start, end time.Time) Window {
  61. return Window{
  62. start: &start,
  63. end: &end,
  64. }
  65. }
  66. // ParseWindowUTC attempts to parse the given string into a valid Window. It
  67. // accepts several formats, returning an error if the given string does not
  68. // match one of the following:
  69. // - named intervals: "today", "yesterday", "week", "month", "lastweek", "lastmonth"
  70. // - durations: "24h", "7d", etc.
  71. // - date ranges: "2020-04-01T00:00:00Z,2020-04-03T00:00:00Z", etc.
  72. // - timestamp ranges: "1586822400,1586908800", etc.
  73. func ParseWindowUTC(window string) (Window, error) {
  74. return parseWindow(window, time.Now().UTC())
  75. }
  76. // ParseWindowWithOffsetString parses the given window string within the context of
  77. // the timezone defined by the UTC offset string of format -07:00, +01:30, etc.
  78. func ParseWindowWithOffsetString(window string, offset string) (Window, error) {
  79. if offset == "UTC" || offset == "" {
  80. return ParseWindowUTC(window)
  81. }
  82. match := offesetRegex.FindStringSubmatch(offset)
  83. if match == nil {
  84. return Window{}, fmt.Errorf("illegal UTC offset: '%s'; should be of form '-07:00'", offset)
  85. }
  86. sig := 1
  87. if match[1] == "-" {
  88. sig = -1
  89. }
  90. hrs64, _ := strconv.ParseInt(match[2], 10, 64)
  91. hrs := sig * int(hrs64)
  92. mins64, _ := strconv.ParseInt(match[3], 10, 64)
  93. mins := sig * int(mins64)
  94. loc := time.FixedZone(fmt.Sprintf("UTC%s", offset), (hrs*60*60)+(mins*60))
  95. now := time.Now().In(loc)
  96. return parseWindow(window, now)
  97. }
  98. // ParseWindowWithOffset parses the given window string within the context of
  99. // the timezone defined by the UTC offset.
  100. func ParseWindowWithOffset(window string, offset time.Duration) (Window, error) {
  101. loc := time.FixedZone("", int(offset.Seconds()))
  102. now := time.Now().In(loc)
  103. return parseWindow(window, now)
  104. }
  105. // parseWindow generalizes the parsing of window strings, relative to a given
  106. // moment in time, defined as "now".
  107. func parseWindow(window string, now time.Time) (Window, error) {
  108. // compute UTC offset in terms of minutes
  109. offHr := now.UTC().Hour() - now.Hour()
  110. offMin := (now.UTC().Minute() - now.Minute()) + (offHr * 60)
  111. offset := time.Duration(offMin) * time.Minute
  112. if window == "today" {
  113. start := now
  114. start = start.Truncate(time.Hour * 24)
  115. start = start.Add(offset)
  116. end := start.Add(time.Hour * 24)
  117. return NewWindow(&start, &end), nil
  118. }
  119. if window == "yesterday" {
  120. start := now
  121. start = start.Truncate(time.Hour * 24)
  122. start = start.Add(offset)
  123. start = start.Add(time.Hour * -24)
  124. end := start.Add(time.Hour * 24)
  125. return NewWindow(&start, &end), nil
  126. }
  127. if window == "week" {
  128. // now
  129. start := now
  130. // 00:00 today, accounting for timezone offset
  131. start = start.Truncate(time.Hour * 24)
  132. start = start.Add(offset)
  133. // 00:00 Sunday of the current week
  134. start = start.Add(-24 * time.Hour * time.Duration(start.Weekday()))
  135. end := now
  136. return NewWindow(&start, &end), nil
  137. }
  138. if window == "lastweek" {
  139. // now
  140. start := now
  141. // 00:00 today, accounting for timezone offset
  142. start = start.Truncate(time.Hour * 24)
  143. start = start.Add(offset)
  144. // 00:00 Sunday of last week
  145. start = start.Add(-24 * time.Hour * time.Duration(start.Weekday()+7))
  146. end := start.Add(7 * 24 * time.Hour)
  147. return NewWindow(&start, &end), nil
  148. }
  149. if window == "month" {
  150. // now
  151. start := now
  152. // 00:00 today, accounting for timezone offset
  153. start = start.Truncate(time.Hour * 24)
  154. start = start.Add(offset)
  155. // 00:00 1st of this month
  156. start = start.Add(-24 * time.Hour * time.Duration(start.Day()-1))
  157. end := now
  158. return NewWindow(&start, &end), nil
  159. }
  160. if window == "month" {
  161. // now
  162. start := now
  163. // 00:00 today, accounting for timezone offset
  164. start = start.Truncate(time.Hour * 24)
  165. start = start.Add(offset)
  166. // 00:00 1st of this month
  167. start = start.Add(-24 * time.Hour * time.Duration(start.Day()-1))
  168. end := now
  169. return NewWindow(&start, &end), nil
  170. }
  171. if window == "lastmonth" {
  172. // now
  173. end := now
  174. // 00:00 today, accounting for timezone offset
  175. end = end.Truncate(time.Hour * 24)
  176. end = end.Add(offset)
  177. // 00:00 1st of this month
  178. end = end.Add(-24 * time.Hour * time.Duration(end.Day()-1))
  179. // 00:00 last day of last month
  180. start := end.Add(-24 * time.Hour)
  181. // 00:00 1st of last month
  182. start = start.Add(-24 * time.Hour * time.Duration(start.Day()-1))
  183. return NewWindow(&start, &end), nil
  184. }
  185. // Match duration strings; e.g. "45m", "24h", "7d"
  186. match := durationRegex.FindStringSubmatch(window)
  187. if match != nil {
  188. dur := time.Minute
  189. if match[2] == "h" {
  190. dur = time.Hour
  191. }
  192. if match[2] == "d" {
  193. dur = 24 * time.Hour
  194. }
  195. num, _ := strconv.ParseInt(match[1], 10, 64)
  196. end := now
  197. start := end.Add(-time.Duration(num) * dur)
  198. return NewWindow(&start, &end), nil
  199. }
  200. // Match duration strings with offset; e.g. "45m offset 15m", etc.
  201. match = durationOffsetRegex.FindStringSubmatch(window)
  202. if match != nil {
  203. end := now
  204. offUnit := time.Minute
  205. if match[4] == "h" {
  206. offUnit = time.Hour
  207. }
  208. if match[4] == "d" {
  209. offUnit = 24 * time.Hour
  210. }
  211. offNum, _ := strconv.ParseInt(match[3], 10, 64)
  212. end = end.Add(-time.Duration(offNum) * offUnit)
  213. durUnit := time.Minute
  214. if match[2] == "h" {
  215. durUnit = time.Hour
  216. }
  217. if match[2] == "d" {
  218. durUnit = 24 * time.Hour
  219. }
  220. durNum, _ := strconv.ParseInt(match[1], 10, 64)
  221. start := end.Add(-time.Duration(durNum) * durUnit)
  222. return NewWindow(&start, &end), nil
  223. }
  224. // Match timestamp pairs, e.g. "1586822400,1586908800" or "1586822400-1586908800"
  225. match = timestampPairRegex.FindStringSubmatch(window)
  226. if match != nil {
  227. s, _ := strconv.ParseInt(match[1], 10, 64)
  228. e, _ := strconv.ParseInt(match[2], 10, 64)
  229. start := time.Unix(s, 0).UTC()
  230. end := time.Unix(e, 0).UTC()
  231. return NewWindow(&start, &end), nil
  232. }
  233. // Match RFC3339 pairs, e.g. "2020-04-01T00:00:00Z,2020-04-03T00:00:00Z"
  234. match = rfcRegex.FindStringSubmatch(window)
  235. if match != nil {
  236. start, _ := time.Parse(time.RFC3339, match[1])
  237. end, _ := time.Parse(time.RFC3339, match[2])
  238. return NewWindow(&start, &end), nil
  239. }
  240. return Window{nil, nil}, fmt.Errorf("illegal window: %s", window)
  241. }
  242. // ApproximatelyEqual returns true if the start and end times of the two windows,
  243. // respectively, are within the given threshold of each other.
  244. func (w Window) ApproximatelyEqual(that Window, threshold time.Duration) bool {
  245. return approxEqual(w.start, that.start, threshold) && approxEqual(w.end, that.end, threshold)
  246. }
  247. func approxEqual(x *time.Time, y *time.Time, threshold time.Duration) bool {
  248. // both times are nil, so they are equal
  249. if x == nil && y == nil {
  250. return true
  251. }
  252. // one time is nil, but the other is not, so they are not equal
  253. if x == nil || y == nil {
  254. return false
  255. }
  256. // neither time is nil, so they are approximately close if their times are
  257. // within the given threshold
  258. delta := math.Abs((*x).Sub(*y).Seconds())
  259. return delta < threshold.Seconds()
  260. }
  261. func (w Window) Clone() Window {
  262. var start, end *time.Time
  263. var s, e time.Time
  264. if w.start != nil {
  265. s = *w.start
  266. start = &s
  267. }
  268. if w.end != nil {
  269. e = *w.end
  270. end = &e
  271. }
  272. return NewWindow(start, end)
  273. }
  274. func (w Window) Contains(t time.Time) bool {
  275. if w.start != nil && t.Before(*w.start) {
  276. return false
  277. }
  278. if w.end != nil && t.After(*w.end) {
  279. return false
  280. }
  281. return true
  282. }
  283. func (w Window) ContainsWindow(that Window) bool {
  284. // only support containing closed windows for now
  285. // could check if openness is compatible with closure
  286. if that.IsOpen() {
  287. return false
  288. }
  289. return w.Contains(*that.start) && w.Contains(*that.end)
  290. }
  291. func (w Window) Duration() time.Duration {
  292. if w.IsOpen() {
  293. // TODO test
  294. return time.Duration(math.Inf(1.0))
  295. }
  296. return w.end.Sub(*w.start)
  297. }
  298. func (w Window) End() *time.Time {
  299. return w.end
  300. }
  301. func (w Window) Equal(that Window) bool {
  302. if w.start != nil && that.start != nil && !w.start.Equal(*that.start) {
  303. // starts are not nil, but not equal
  304. return false
  305. }
  306. if w.end != nil && that.end != nil && !w.end.Equal(*that.end) {
  307. // ends are not nil, but not equal
  308. return false
  309. }
  310. if (w.start == nil && that.start != nil) || (w.start != nil && that.start == nil) {
  311. // one start is nil, the other is not
  312. return false
  313. }
  314. if (w.end == nil && that.end != nil) || (w.end != nil && that.end == nil) {
  315. // one end is nil, the other is not
  316. return false
  317. }
  318. // either both starts are nil, or they match; likewise for the ends
  319. return true
  320. }
  321. func (w Window) ExpandStart(start time.Time) Window {
  322. if w.start == nil || start.Before(*w.start) {
  323. w.start = &start
  324. }
  325. return w
  326. }
  327. func (w Window) ExpandEnd(end time.Time) Window {
  328. if w.end == nil || end.After(*w.end) {
  329. w.end = &end
  330. }
  331. return w
  332. }
  333. func (w Window) Expand(that Window) Window {
  334. if that.start == nil {
  335. w.start = nil
  336. } else {
  337. w = w.ExpandStart(*that.start)
  338. }
  339. if that.end == nil {
  340. w.end = nil
  341. } else {
  342. w = w.ExpandEnd(*that.end)
  343. }
  344. return w
  345. }
  346. func (w Window) ContractStart(start time.Time) Window {
  347. if w.start == nil || start.After(*w.start) {
  348. w.start = &start
  349. }
  350. return w
  351. }
  352. func (w Window) ContractEnd(end time.Time) Window {
  353. if w.end == nil || end.Before(*w.end) {
  354. w.end = &end
  355. }
  356. return w
  357. }
  358. func (w Window) Contract(that Window) Window {
  359. if that.start != nil {
  360. w = w.ContractStart(*that.start)
  361. }
  362. if that.end != nil {
  363. w = w.ContractEnd(*that.end)
  364. }
  365. return w
  366. }
  367. func (w Window) Hours() float64 {
  368. if w.IsOpen() {
  369. return math.Inf(1)
  370. }
  371. return w.end.Sub(*w.start).Hours()
  372. }
  373. // IsEmpty a Window is empty if it does not have a start and an end
  374. func (w Window) IsEmpty() bool {
  375. return w.start == nil && w.end == nil
  376. }
  377. // HasDuration a Window has duration if neither start and end are not nil and not equal
  378. func (w Window) HasDuration() bool {
  379. return !w.IsOpen() && !w.end.Equal(*w.Start())
  380. }
  381. // IsNegative a Window is negative if start and end are not null and end is before start
  382. func (w Window) IsNegative() bool {
  383. return !w.IsOpen() && w.end.Before(*w.Start())
  384. }
  385. // IsOpen a Window is open if it has a nil start or end
  386. func (w Window) IsOpen() bool {
  387. return w.start == nil || w.end == nil
  388. }
  389. // TODO:CLEANUP make this unmarshalable (make Start and End public)
  390. func (w Window) MarshalJSON() ([]byte, error) {
  391. buffer := bytes.NewBufferString("{")
  392. if w.start != nil {
  393. buffer.WriteString(fmt.Sprintf("\"start\":\"%s\",", w.start.Format(time.RFC3339)))
  394. } else {
  395. buffer.WriteString(fmt.Sprintf("\"start\":\"%s\",", "null"))
  396. }
  397. if w.end != nil {
  398. buffer.WriteString(fmt.Sprintf("\"end\":\"%s\"", w.end.Format(time.RFC3339)))
  399. } else {
  400. buffer.WriteString(fmt.Sprintf("\"end\":\"%s\"", "null"))
  401. }
  402. buffer.WriteString("}")
  403. return buffer.Bytes(), nil
  404. }
  405. func (w Window) Minutes() float64 {
  406. if w.IsOpen() {
  407. return math.Inf(1)
  408. }
  409. return w.end.Sub(*w.start).Minutes()
  410. }
  411. // Overlaps returns true iff the two given Windows share an amount of temporal
  412. // coverage.
  413. // TODO complete (with unit tests!) and then implement in AllocationSet.accumulate
  414. // TODO:CLEANUP
  415. // func (w Window) Overlaps(x Window) bool {
  416. // if (w.start == nil && w.end == nil) || (x.start == nil && x.end == nil) {
  417. // // one window is completely open, so overlap is guaranteed
  418. // // <---------->
  419. // // ?------?
  420. // return true
  421. // }
  422. // // Neither window is completely open (nil, nil), but one or the other might
  423. // // still be future- or past-open.
  424. // if w.start == nil {
  425. // // w is past-open, future-closed
  426. // // <------]
  427. // if x.start != nil && !x.start.Before(*w.end) {
  428. // // x starts after w ends (or eq)
  429. // // <------]
  430. // // [------?
  431. // return false
  432. // }
  433. // // <-----]
  434. // // ?-----?
  435. // return true
  436. // }
  437. // if w.end == nil {
  438. // // w is future-open, past-closed
  439. // // [------>
  440. // if x.end != nil && !x.end.After(*w.end) {
  441. // // x ends before w begins (or eq)
  442. // // [------>
  443. // // ?------]
  444. // return false
  445. // }
  446. // // [------>
  447. // // ?------?
  448. // return true
  449. // }
  450. // // Now we know w is closed, but we don't know about x
  451. // // [------]
  452. // // ?------?
  453. // if x.start == nil {
  454. // // TODO
  455. // }
  456. // if x.end == nil {
  457. // // TODO
  458. // }
  459. // // Both are closed.
  460. // if !x.start.Before(*w.end) && !x.end.Before(*w.end) {
  461. // // x starts and ends after w ends
  462. // // [------]
  463. // // [------]
  464. // return false
  465. // }
  466. // if !x.start.After(*w.start) && !x.end.After(*w.start) {
  467. // // x starts and ends before w starts
  468. // // [------]
  469. // // [------]
  470. // return false
  471. // }
  472. // // w and x must overlap
  473. // // [------]
  474. // // [------]
  475. // return true
  476. // }
  477. func (w Window) Set(start, end *time.Time) {
  478. w.start = start
  479. w.end = end
  480. }
  481. // Shift adds the given duration to both the start and end times of the window
  482. func (w Window) Shift(dur time.Duration) Window {
  483. if w.start != nil {
  484. s := w.start.Add(dur)
  485. w.start = &s
  486. }
  487. if w.end != nil {
  488. e := w.end.Add(dur)
  489. w.end = &e
  490. }
  491. return w
  492. }
  493. func (w Window) Start() *time.Time {
  494. return w.start
  495. }
  496. func (w Window) String() string {
  497. if w.start == nil && w.end == nil {
  498. return "[nil, nil)"
  499. }
  500. if w.start == nil {
  501. return fmt.Sprintf("[nil, %s)", w.end.Format("2006-01-02T15:04:05-0700"))
  502. }
  503. if w.end == nil {
  504. return fmt.Sprintf("[%s, nil)", w.start.Format("2006-01-02T15:04:05-0700"))
  505. }
  506. return fmt.Sprintf("[%s, %s)", w.start.Format("2006-01-02T15:04:05-0700"), w.end.Format("2006-01-02T15:04:05-0700"))
  507. }
  508. // DurationOffset returns durations representing the duration and offset of the
  509. // given window
  510. func (w Window) DurationOffset() (time.Duration, time.Duration, error) {
  511. if w.IsOpen() || w.IsNegative() {
  512. return 0, 0, fmt.Errorf("illegal window: %s", w)
  513. }
  514. duration := w.Duration()
  515. offset := time.Since(*w.End())
  516. return duration, offset, nil
  517. }
  518. // DurationOffsetForPrometheus returns strings representing durations for the
  519. // duration and offset of the given window, factoring in the Thanos offset if
  520. // necessary. Whereas duration is a simple duration string (e.g. "1d"), the
  521. // offset includes the word "offset" (e.g. " offset 2d") so that the values
  522. // returned can be used directly in the formatting string "some_metric[%s]%s"
  523. // to generate the query "some_metric[1d] offset 2d".
  524. func (w Window) DurationOffsetForPrometheus() (string, string, error) {
  525. duration, offset, err := w.DurationOffset()
  526. if err != nil {
  527. return "", "", err
  528. }
  529. // If using Thanos, increase offset to 3 hours, reducing the duration by
  530. // equal measure to maintain the same starting point.
  531. thanosDur := thanos.OffsetDuration()
  532. if offset < thanosDur && env.IsThanosEnabled() {
  533. diff := thanosDur - offset
  534. offset += diff
  535. duration -= diff
  536. }
  537. // If duration < 0, return an error
  538. if duration < 0 {
  539. return "", "", fmt.Errorf("negative duration: %s", duration)
  540. }
  541. // Negative offset means that the end time is in the future. Prometheus
  542. // fails for non-positive offset values, so shrink the duration and
  543. // remove the offset altogether.
  544. if offset < 0 {
  545. duration = duration + offset
  546. offset = 0
  547. }
  548. durStr, offStr := timeutil.DurationOffsetStrings(duration, offset)
  549. if offset < time.Minute {
  550. offStr = ""
  551. } else {
  552. offStr = " offset " + offStr
  553. }
  554. return durStr, offStr, nil
  555. }
  556. // DurationOffsetStrings returns formatted, Prometheus-compatible strings representing
  557. // the duration and offset of the window in terms of days, hours, minutes, or seconds;
  558. // e.g. ("7d", "1441m", "30m", "1s", "")
  559. func (w Window) DurationOffsetStrings() (string, string) {
  560. dur, off, err := w.DurationOffset()
  561. if err != nil {
  562. return "", ""
  563. }
  564. return timeutil.DurationOffsetStrings(dur, off)
  565. }
  566. // GetPercentInWindow Determine pct of item time contained the window.
  567. // determined by the overlap of the start/end with the given
  568. // window, which will be negative if there is no overlap. If
  569. // there is positive overlap, compare it with the total mins.
  570. //
  571. // e.g. here are the two possible scenarios as simplidied
  572. // 10m windows with dashes representing item's time running:
  573. //
  574. // 1. item falls entirely within one CloudCostItemSet window
  575. // | ---- | | |
  576. // totalMins = 4.0
  577. // pct := 4.0 / 4.0 = 1.0 for window 1
  578. // pct := 0.0 / 4.0 = 0.0 for window 2
  579. // pct := 0.0 / 4.0 = 0.0 for window 3
  580. //
  581. // 2. item overlaps multiple CloudCostItemSet windows
  582. // | ----|----------|-- |
  583. // totalMins = 16.0
  584. // pct := 4.0 / 16.0 = 0.250 for window 1
  585. // pct := 10.0 / 16.0 = 0.625 for window 2
  586. // pct := 2.0 / 16.0 = 0.125 for window 3
  587. func (w Window) GetPercentInWindow(that Window) float64 {
  588. if that.IsOpen() {
  589. log.Errorf("Window: GetPercentInWindow: invalid window %s", that.String())
  590. return 0
  591. }
  592. s := *that.Start()
  593. if s.Before(*w.Start()) {
  594. s = *w.Start()
  595. }
  596. e := *that.End()
  597. if e.After(*w.End()) {
  598. e = *w.End()
  599. }
  600. mins := e.Sub(s).Minutes()
  601. if mins <= 0.0 {
  602. return 0.0
  603. }
  604. totalMins := that.Duration().Minutes()
  605. pct := mins / totalMins
  606. return pct
  607. }
  608. // GetWindows returns a slice of Window with equal size between the given start and end. If windowSize does not evenly
  609. // divide the period between start and end, the last window is not added
  610. func GetWindows(start time.Time, end time.Time, windowSize time.Duration) ([]Window, error) {
  611. // Ensure the range is evenly divisible into windows of the given duration
  612. dur := end.Sub(start)
  613. if int(dur.Minutes())%int(windowSize.Minutes()) != 0 {
  614. return nil, fmt.Errorf("range not divisible by window: [%s, %s] by %s", start, end, windowSize)
  615. }
  616. // Ensure that provided times are multiples of the provided windowSize (e.g. midnight for daily windows, on the hour for hourly windows)
  617. if start != start.Truncate(windowSize) {
  618. return nil, fmt.Errorf("provided times are not divisible by provided window: [%s, %s] by %s", start, end, windowSize)
  619. }
  620. // Ensure timezones match
  621. _, sz := start.Zone()
  622. _, ez := end.Zone()
  623. if sz != ez {
  624. return nil, fmt.Errorf("range has mismatched timezones: %s, %s", start, end)
  625. }
  626. if sz != int(env.GetParsedUTCOffset().Seconds()) {
  627. return nil, fmt.Errorf("range timezone doesn't match configured timezone: expected %s; found %ds", env.GetParsedUTCOffset(), sz)
  628. }
  629. // Build array of windows to cover the CloudCostItemSetRange
  630. windows := []Window{}
  631. s, e := start, start.Add(windowSize)
  632. for !e.After(end) {
  633. ws := s
  634. we := e
  635. windows = append(windows, NewWindow(&ws, &we))
  636. s = s.Add(windowSize)
  637. e = e.Add(windowSize)
  638. }
  639. return windows, nil
  640. }
  641. // GetWindowsForQueryWindow breaks up a window into an array of windows with a max size of queryWindow
  642. func GetWindowsForQueryWindow(start time.Time, end time.Time, queryWindow time.Duration) ([]Window, error) {
  643. // Ensure timezones match
  644. _, sz := start.Zone()
  645. _, ez := end.Zone()
  646. if sz != ez {
  647. return nil, fmt.Errorf("range has mismatched timezones: %s, %s", start, end)
  648. }
  649. if sz != int(env.GetParsedUTCOffset().Seconds()) {
  650. return nil, fmt.Errorf("range timezone doesn't match configured timezone: expected %s; found %ds", env.GetParsedUTCOffset(), sz)
  651. }
  652. // Build array of windows to cover the CloudCostItemSetRange
  653. windows := []Window{}
  654. s, e := start, start.Add(queryWindow)
  655. for s.Before(end) {
  656. ws := s
  657. we := e
  658. windows = append(windows, NewWindow(&ws, &we))
  659. s = s.Add(queryWindow)
  660. e = e.Add(queryWindow)
  661. if e.After(end) {
  662. e = end
  663. }
  664. }
  665. return windows, nil
  666. }
  667. type BoundaryError struct {
  668. Requested Window
  669. Supported Window
  670. Message string
  671. }
  672. func NewBoundaryError(req, sup Window, msg string) *BoundaryError {
  673. return &BoundaryError{
  674. Requested: req,
  675. Supported: sup,
  676. Message: msg,
  677. }
  678. }
  679. func (be *BoundaryError) Error() string {
  680. if be == nil {
  681. return "<nil>"
  682. }
  683. return fmt.Sprintf("boundary error: requested %s; supported %s: %s", be.Requested, be.Supported, be.Message)
  684. }