queryservice_helper.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. package inferencecost
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/opencost/opencost/core/pkg/opencost"
  6. "github.com/opencost/opencost/core/pkg/util/mapper"
  7. "github.com/opencost/opencost/core/pkg/util/timeutil"
  8. )
  9. // QueryRequest holds the parsed parameters for an inference cost query.
  10. type QueryRequest struct {
  11. Start time.Time
  12. End time.Time
  13. CostBasis CostBasis
  14. AggregateBy []string
  15. Accumulate opencost.AccumulateOption
  16. Filter []filterSpec
  17. Step time.Duration
  18. }
  19. // ParseInferenceCostRequest parses the common query parameters from an HTTP
  20. // request into a QueryRequest. It mirrors the pattern used by
  21. // customcost.ParseCustomCostTotalRequest.
  22. //
  23. // Required: window (RFC3339 start,end or named range).
  24. // Optional: costBasis (default: allocation), aggregate, accumulate, filter.
  25. //
  26. // The step field is derived from the accumulate option.
  27. func ParseInferenceCostRequest(qp mapper.PrimitiveMap) (*QueryRequest, error) {
  28. // --- window (required) ---
  29. windowStr := qp.Get("window", "")
  30. if windowStr == "" {
  31. return nil, fmt.Errorf("missing required 'window' parameter")
  32. }
  33. window, err := opencost.ParseWindowUTC(windowStr)
  34. if err != nil {
  35. return nil, fmt.Errorf("invalid 'window' parameter: %w", err)
  36. }
  37. if window.IsOpen() {
  38. return nil, fmt.Errorf("invalid 'window' parameter: window must have both start and end: %s", windowStr)
  39. }
  40. // --- costBasis (optional, default: allocation) ---
  41. basisStr := qp.Get("costBasis", string(CostBasisAllocation))
  42. var basis CostBasis
  43. switch basisStr {
  44. case string(CostBasisUsage):
  45. basis = CostBasisUsage
  46. case string(CostBasisAllocation):
  47. basis = CostBasisAllocation
  48. default:
  49. return nil, fmt.Errorf("invalid 'costBasis' parameter %q: must be %q or %q", basisStr, CostBasisUsage, CostBasisAllocation)
  50. }
  51. // --- aggregate (optional) ---
  52. aggregateByRaw := qp.GetList("aggregate", ",")
  53. for _, dim := range aggregateByRaw {
  54. if !supportedAggregateProperties[dim] {
  55. return nil, fmt.Errorf("unsupported 'aggregate' dimension %q: supported dimensions are model_name, model_version, namespace, cluster, pod, controller, controller_kind, container, workload_type", dim)
  56. }
  57. }
  58. // --- accumulate (optional, defaults to none) ---
  59. accumulate := opencost.ParseAccumulate(qp.Get("accumulate", ""))
  60. // --- filter (optional) ---
  61. filterSpecs, err := parseFilter(qp.Get("filter", ""))
  62. if err != nil {
  63. return nil, fmt.Errorf("invalid 'filter' parameter: %w", err)
  64. }
  65. // Derive step from accumulate. When no accumulate is requested the step
  66. // spans the full window (produces a single data point).
  67. step := stepFromAccumulate(accumulate, *window.Start(), *window.End())
  68. return &QueryRequest{
  69. Start: *window.Start(),
  70. End: *window.End(),
  71. CostBasis: basis,
  72. AggregateBy: aggregateByRaw,
  73. Accumulate: accumulate,
  74. Filter: filterSpecs,
  75. Step: step,
  76. }, nil
  77. }
  78. // ParseInferenceCostTimeseriesRequest is identical to ParseInferenceCostRequest
  79. // but requires the accumulate parameter (timeseries must have a defined step).
  80. func ParseInferenceCostTimeseriesRequest(qp mapper.PrimitiveMap) (*QueryRequest, error) {
  81. req, err := ParseInferenceCostRequest(qp)
  82. if err != nil {
  83. return nil, err
  84. }
  85. if qp.Get("accumulate", "") == "" {
  86. return nil, fmt.Errorf("missing required 'accumulate' parameter for timeseries endpoint: must be one of hour, day, week, month")
  87. }
  88. if req.Accumulate == opencost.AccumulateOptionNone {
  89. return nil, fmt.Errorf("invalid 'accumulate' parameter for timeseries endpoint: must be one of hour, day, week, month")
  90. }
  91. return req, nil
  92. }
  93. // stepFromAccumulate returns the time.Duration that corresponds to an
  94. // AccumulateOption. When AccumulateOptionNone is given (or not recognised),
  95. // the full window duration is used so the loop produces a single pass.
  96. //
  97. // This is a local reimplementation of the package-private helpers in
  98. // pkg/costmodel (resolveStepFromQuery / resolveStepForAccumulate) — kept here
  99. // to preserve the domain package's self-containment.
  100. func stepFromAccumulate(opt opencost.AccumulateOption, start, end time.Time) time.Duration {
  101. switch opt {
  102. case opencost.AccumulateOptionHour:
  103. return time.Hour
  104. case opencost.AccumulateOptionDay:
  105. return timeutil.Day
  106. case opencost.AccumulateOptionWeek:
  107. return timeutil.Week
  108. case opencost.AccumulateOptionMonth:
  109. // Steps by 30 days (consistent with
  110. // how OpenCost's CustomCost and CloudCost handle monthly accumulation).
  111. return timeutil.Day * 30
  112. default:
  113. // No accumulation: single window covering the full request range.
  114. d := end.Sub(start)
  115. if d <= 0 {
  116. d = timeutil.Day
  117. }
  118. return d
  119. }
  120. }