| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- package inferencecost
- import (
- "fmt"
- "time"
- "github.com/opencost/opencost/core/pkg/opencost"
- "github.com/opencost/opencost/core/pkg/util/mapper"
- "github.com/opencost/opencost/core/pkg/util/timeutil"
- )
- // QueryRequest holds the parsed parameters for an inference cost query.
- type QueryRequest struct {
- Start time.Time
- End time.Time
- CostBasis CostBasis
- AggregateBy []string
- Accumulate opencost.AccumulateOption
- Filter []filterSpec
- Step time.Duration
- }
- // ParseInferenceCostRequest parses the common query parameters from an HTTP
- // request into a QueryRequest. It mirrors the pattern used by
- // customcost.ParseCustomCostTotalRequest.
- //
- // Required: window (RFC3339 start,end or named range).
- // Optional: costBasis (default: allocation), aggregate, accumulate, filter.
- //
- // The step field is derived from the accumulate option.
- func ParseInferenceCostRequest(qp mapper.PrimitiveMap) (*QueryRequest, error) {
- // --- window (required) ---
- windowStr := qp.Get("window", "")
- if windowStr == "" {
- return nil, fmt.Errorf("missing required 'window' parameter")
- }
- window, err := opencost.ParseWindowUTC(windowStr)
- if err != nil {
- return nil, fmt.Errorf("invalid 'window' parameter: %w", err)
- }
- if window.IsOpen() {
- return nil, fmt.Errorf("invalid 'window' parameter: window must have both start and end: %s", windowStr)
- }
- // --- costBasis (optional, default: allocation) ---
- basisStr := qp.Get("costBasis", string(CostBasisAllocation))
- var basis CostBasis
- switch basisStr {
- case string(CostBasisUsage):
- basis = CostBasisUsage
- case string(CostBasisAllocation):
- basis = CostBasisAllocation
- default:
- return nil, fmt.Errorf("invalid 'costBasis' parameter %q: must be %q or %q", basisStr, CostBasisUsage, CostBasisAllocation)
- }
- // --- aggregate (optional) ---
- aggregateByRaw := qp.GetList("aggregate", ",")
- for _, dim := range aggregateByRaw {
- if !supportedAggregateProperties[dim] {
- 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)
- }
- }
- // --- accumulate (optional, defaults to none) ---
- accumulate := opencost.ParseAccumulate(qp.Get("accumulate", ""))
- // --- filter (optional) ---
- filterSpecs, err := parseFilter(qp.Get("filter", ""))
- if err != nil {
- return nil, fmt.Errorf("invalid 'filter' parameter: %w", err)
- }
- // Derive step from accumulate. When no accumulate is requested the step
- // spans the full window (produces a single data point).
- step := stepFromAccumulate(accumulate, *window.Start(), *window.End())
- return &QueryRequest{
- Start: *window.Start(),
- End: *window.End(),
- CostBasis: basis,
- AggregateBy: aggregateByRaw,
- Accumulate: accumulate,
- Filter: filterSpecs,
- Step: step,
- }, nil
- }
- // ParseInferenceCostTimeseriesRequest is identical to ParseInferenceCostRequest
- // but requires the accumulate parameter (timeseries must have a defined step).
- func ParseInferenceCostTimeseriesRequest(qp mapper.PrimitiveMap) (*QueryRequest, error) {
- req, err := ParseInferenceCostRequest(qp)
- if err != nil {
- return nil, err
- }
- if qp.Get("accumulate", "") == "" {
- return nil, fmt.Errorf("missing required 'accumulate' parameter for timeseries endpoint: must be one of hour, day, week, month")
- }
- if req.Accumulate == opencost.AccumulateOptionNone {
- return nil, fmt.Errorf("invalid 'accumulate' parameter for timeseries endpoint: must be one of hour, day, week, month")
- }
- return req, nil
- }
- // stepFromAccumulate returns the time.Duration that corresponds to an
- // AccumulateOption. When AccumulateOptionNone is given (or not recognised),
- // the full window duration is used so the loop produces a single pass.
- //
- // This is a local reimplementation of the package-private helpers in
- // pkg/costmodel (resolveStepFromQuery / resolveStepForAccumulate) — kept here
- // to preserve the domain package's self-containment.
- func stepFromAccumulate(opt opencost.AccumulateOption, start, end time.Time) time.Duration {
- switch opt {
- case opencost.AccumulateOptionHour:
- return time.Hour
- case opencost.AccumulateOptionDay:
- return timeutil.Day
- case opencost.AccumulateOptionWeek:
- return timeutil.Week
- case opencost.AccumulateOptionMonth:
- // Steps by 30 days (consistent with
- // how OpenCost's CustomCost and CloudCost handle monthly accumulation).
- return timeutil.Day * 30
- default:
- // No accumulation: single window covering the full request range.
- d := end.Sub(start)
- if d <= 0 {
- d = timeutil.Day
- }
- return d
- }
- }
|