diff --git a/changelog/28068.txt b/changelog/28068.txt new file mode 100644 index 0000000000..ec608a67a1 --- /dev/null +++ b/changelog/28068.txt @@ -0,0 +1,7 @@ +```release-note:improvement +cli: `vault operator usage` will now include a warning if the specified usage period contains estimated client counts. +``` + +```release-note:improvement +activity: `/sys/internal/counters/activity` will now include a warning if the specified usage period contains estimated client counts. +``` \ No newline at end of file diff --git a/command/operator_usage.go b/command/operator_usage.go index 199c541036..e96b0ca33d 100644 --- a/command/operator_usage.go +++ b/command/operator_usage.go @@ -142,6 +142,12 @@ func (c *OperatorUsageCommand) Run(args []string) int { colConfig.Empty = " " // Do not show n/a on intentional blank lines colConfig.Glue = " " c.UI.Output(tableOutput(out, colConfig)) + + // Also, output the warnings returned, if any: + for _, warning := range resp.Warnings { + c.UI.Warn(warning) + } + return 0 } diff --git a/vault/logical_system_activity.go b/vault/logical_system_activity.go index 224480d391..56f9ecb665 100644 --- a/vault/logical_system_activity.go +++ b/vault/logical_system_activity.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/helper/timeutil" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) @@ -25,6 +26,9 @@ var defaultToRetentionMonthsMaxWarning = fmt.Sprintf("retention_months cannot be const ( // WarningCurrentBillingPeriodDeprecated is a warning string that is used to indicate that the current_billing_period field, as the default start time will automatically be the billing period start date WarningCurrentBillingPeriodDeprecated = "current_billing_period is deprecated; unless otherwise specified, all requests will default to the current billing period" + + // WarningCurrentMonthIsAnEstimate is a warning string that is used to let the customer know that for this query, the current month's data is estimated. + WarningCurrentMonthIsAnEstimate = "Since this usage period includes both the current month and at least one historical month, counts returned in this usage period are an estimate. Client counts for this period will no longer be estimated at the start of the next month." ) // activityQueryPath is available in every namespace @@ -224,7 +228,35 @@ func (b *SystemBackend) rootActivityPaths() []*framework.Path { return paths } -func parseStartEndTimes(a *ActivityLog, d *framework.FieldData, billingStartTime time.Time) (time.Time, time.Time, error) { +// queryContainsEstimates calculates if the query for client counts will contain estimates. +// A query between months N-2 and N-1 would not be an estimate, as with a query for month N. +// But a query between N-2 and N or N-1 and N would be an estimate. +func queryContainsEstimates(startTime time.Time, endTime time.Time) bool { + startTime = timeutil.EndOfMonth(startTime) + endTime = timeutil.EndOfMonth(endTime) + + if startTime == endTime { + // If we're only estimating the current month, then we have no estimation + return false + } + + if timeutil.IsCurrentMonth(endTime, time.Now().UTC()) { + // Our query includes the current month and previous months, so we have estimation + return true + } + + // If the endTime is in the future, the behaviour is equivalent to when endTime is set to the current month + // (it includes the current month and previous months, so we have estimation) + endOfCurrentMonth := timeutil.EndOfMonth(time.Now().UTC()) + if endTime.After(endOfCurrentMonth) { + return true + } + + // Our query doesn't include the current month + return false +} + +func parseStartEndTimes(d *framework.FieldData, billingStartTime time.Time) (time.Time, time.Time, error) { startTime := d.Get("start_time").(time.Time) endTime := d.Get("end_time").(time.Time) @@ -262,7 +294,7 @@ func (b *SystemBackend) handleClientExport(ctx context.Context, req *logical.Req return logical.ErrorResponse("no activity log present"), nil } - startTime, endTime, err := parseStartEndTimes(a, d, b.Core.BillingStart()) + startTime, endTime, err := parseStartEndTimes(d, b.Core.BillingStart()) if err != nil { return logical.ErrorResponse(err.Error()), nil } @@ -315,7 +347,7 @@ func (b *SystemBackend) handleClientMetricQuery(ctx context.Context, req *logica } var err error - startTime, endTime, err = parseStartEndTimes(a, d, b.Core.BillingStart()) + startTime, endTime, err = parseStartEndTimes(d, b.Core.BillingStart()) if err != nil { return logical.ErrorResponse(err.Error()), nil } @@ -336,6 +368,10 @@ func (b *SystemBackend) handleClientMetricQuery(ctx context.Context, req *logica return resp204, err } + if queryContainsEstimates(startTime, endTime) { + warnings = append(warnings, WarningCurrentMonthIsAnEstimate) + } + return &logical.Response{ Warnings: warnings, Data: results,