Skip to content

feat(coderd): add user latency and template insights endpoints #8519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8a1d3ee
feat(coderd): add user latency and template insights endpoints
mafredri Jul 14, 2023
b31ec33
feat(coderd): implement user latency insights logic
mafredri Jul 18, 2023
aaadc6a
feat(coderd): implement template insights logic
mafredri Jul 19, 2023
b54b0aa
make start/endtime check more strict
mafredri Jul 19, 2023
5f2bfd4
move interval verification
mafredri Jul 19, 2023
4820c53
gen
mafredri Jul 20, 2023
6326643
allow endtime for today include the hour
mafredri Jul 20, 2023
2198c5f
prevent timetravel
mafredri Jul 20, 2023
2e51056
add test for latencies
mafredri Jul 20, 2023
ddc8606
add test for template insights
mafredri Jul 20, 2023
26e0f02
Merge branch 'main' into mafredri/feat-coderd-add-user-latency-and-in…
mafredri Jul 20, 2023
2c8e311
verify same timezone for start/end
mafredri Jul 20, 2023
917997b
fix typo in query comment and improve comments
mafredri Jul 21, 2023
e5b96d4
fix generic internal server error
mafredri Jul 21, 2023
87b1b90
s/greater/after/
mafredri Jul 21, 2023
bffc673
unexport insights time layout
mafredri Jul 21, 2023
056918e
remove interval none
mafredri Jul 21, 2023
d5a9865
remove mixed tz restrictions, test DST
mafredri Jul 21, 2023
088620e
fix lint
mafredri Jul 21, 2023
397cc7a
add fixmes to dbauthz
mafredri Jul 21, 2023
ebacfe1
improve comment about assert equality
mafredri Jul 21, 2023
cd07478
replace sleep with cat
mafredri Jul 21, 2023
040390d
add bad request tests
mafredri Jul 21, 2023
b1de87f
fix comment placement
mafredri Jul 21, 2023
5588e39
create convert function for builtin apps
mafredri Jul 21, 2023
dafbba1
move interval loop
mafredri Jul 21, 2023
fc2157d
remove all users, improve test comments
mafredri Jul 21, 2023
7fd13df
Merge branch 'main' into mafredri/feat-coderd-add-user-latency-and-in…
mafredri Jul 21, 2023
9e228f6
Merge branch 'main' into mafredri/feat-coderd-add-user-latency-and-in…
mafredri Jul 21, 2023
b03cc29
fix windows test?
mafredri Jul 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
allow endtime for today include the hour
  • Loading branch information
mafredri committed Jul 20, 2023
commit 6326643ef1a839a0a807a6f5e6b80d3a4f8cb653
116 changes: 73 additions & 43 deletions coderd/insights.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,16 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) {
return
}

// TODO(mafredri): Client or deployment timezone?
// Example:
// - I want data from Monday - Friday
// - I'm UTC+3 and the deployment is UTC+0
// - Do we select Monday - Friday in UTC+0 or UTC+3?
// - Considering users can be in different timezones, perhaps this should be per-user (but we don't keep track of user timezones).
p := httpapi.NewQueryParamParser().
Required("start_time").
Required("end_time")
vals := r.URL.Query()
var (
startTime = p.Time3339Nano(vals, time.Time{}, "start_time")
endTime = p.Time3339Nano(vals, time.Time{}, "end_time")
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
// The QueryParamParser does not preserve timezone, so we need
// to parse the time ourselves.
startTimeString = p.String(vals, "", "start_time")
endTimeString = p.String(vals, "", "end_time")
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
)
p.ErrorExcessParams(vals)
if len(p.Errors) > 0 {
Expand All @@ -90,15 +86,11 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) {
return
}

if !verifyInsightsStartAndEndTime(ctx, rw, startTime, endTime) {
startTime, endTime, ok := parseInsightsStartAndEndTime(ctx, rw, startTimeString, endTimeString)
if !ok {
return
}

// Should we verify all template IDs exist, or just return no rows?
// _, err := api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
// IDs: templateIDs,
// })

rows, err := api.Database.GetUserLatencyInsights(ctx, database.GetUserLatencyInsightsParams{
StartTime: startTime,
EndTime: endTime,
Expand Down Expand Up @@ -192,10 +184,12 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
Required("end_time")
vals := r.URL.Query()
var (
startTime = p.Time3339Nano(vals, time.Time{}, "start_time")
endTime = p.Time3339Nano(vals, time.Time{}, "end_time")
intervalString = p.String(vals, string(codersdk.InsightsReportIntervalNone), "interval")
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
// The QueryParamParser does not preserve timezone, so we need
// to parse the time ourselves.
startTimeString = p.String(vals, "", "start_time")
endTimeString = p.String(vals, "", "end_time")
intervalString = p.String(vals, string(codersdk.InsightsReportIntervalNone), "interval")
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
)
p.ErrorExcessParams(vals)
if len(p.Errors) > 0 {
Expand All @@ -206,19 +200,15 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
return
}

if !verifyInsightsStartAndEndTime(ctx, rw, startTime, endTime) {
startTime, endTime, ok := parseInsightsStartAndEndTime(ctx, rw, startTimeString, endTimeString)
if !ok {
return
}
interval, ok := verifyInsightsInterval(ctx, rw, intervalString)
if !ok {
return
}

// Should we verify all template IDs exist, or just return no rows?
// _, err := api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
// IDs: templateIDs,
// })

var usage database.GetTemplateInsightsRow
var dailyUsage []database.GetTemplateDailyInsightsRow
// Use a transaction to ensure that we get consistent data between
Expand Down Expand Up @@ -310,54 +300,94 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, resp)
}

func verifyInsightsStartAndEndTime(ctx context.Context, rw http.ResponseWriter, startTime, endTime time.Time) bool {
for _, v := range []struct {
name string
t time.Time
// parseInsightsStartAndEndTime parses the start and end time query parameters
// and returns the parsed values. The client provided timezone must be preserved
// when parsing the time. Verification is performed so that the start and end
// time are not zero and that the end time is not before the start time. The
// clock must be set to 00:00:00, except for "today", where end time is allowed
// to provide the hour of the day (e.g. 14:00:00).
func parseInsightsStartAndEndTime(ctx context.Context, rw http.ResponseWriter, startTimeString, endTimeString string) (startTime, endTime time.Time, ok bool) {
const insightsTimeLayout = time.RFC3339Nano

for _, qp := range []struct {
name, value string
dest *time.Time
}{
{"start_time", startTime},
{"end_time", endTime},
{"start_time", startTimeString, &startTime},
{"end_time", endTimeString, &endTime},
} {
if v.t.IsZero() {
t, err := time.Parse(insightsTimeLayout, qp.value)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Query parameter has invalid value.",
Validations: []codersdk.ValidationError{
{
Field: v.name,
Detail: "must be not be zero",
Field: qp.name,
Detail: fmt.Sprintf("Query param %q must be a valid date format (%s): %s", qp.name, insightsTimeLayout, err.Error()),
},
},
})
return false
return time.Time{}, time.Time{}, false
}
h, m, s := v.t.Clock()
if h != 0 || m != 0 || s != 0 {
if t.IsZero() {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Query parameter has invalid value.",
Validations: []codersdk.ValidationError{
{
Field: qp.name,
Detail: fmt.Sprintf("Query param %q must not be zero", qp.name),
},
},
})
return time.Time{}, time.Time{}, false
}
ensureZeroHour := true
if qp.name == "end_time" {
ey, em, ed := t.Date()
ty, tm, td := time.Now().Date()

ensureZeroHour = ey != ty || em != tm || ed != td
}
h, m, s := t.Clock()
if ensureZeroHour && (h != 0 || m != 0 || s != 0) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Query parameter has invalid value.",
Validations: []codersdk.ValidationError{
{
Field: qp.name,
Detail: fmt.Sprintf("Query param %q must have the clock set to 00:00:00", qp.name),
},
},
})
return time.Time{}, time.Time{}, false
} else if m != 0 || s != 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Query parameter has invalid value.",
Validations: []codersdk.ValidationError{
{
Field: v.name,
Detail: "clock must be 00:00:00",
Field: qp.name,
Detail: fmt.Sprintf("Query param %q must have the clock set to %02d:00:00", qp.name, h),
},
},
})
return false
return time.Time{}, time.Time{}, false
}
*qp.dest = t
}
if endTime.Before(startTime) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Query parameter has invalid value.",
Validations: []codersdk.ValidationError{
{
Field: "end_time",
Detail: "must be after start_time",
Detail: fmt.Sprintf("Query param %q must be greater than %q", "end_time", "start_time"),
},
},
})
return false
return time.Time{}, time.Time{}, false
}

return true
return startTime, endTime, true
}

func verifyInsightsInterval(ctx context.Context, rw http.ResponseWriter, intervalString string) (codersdk.InsightsReportInterval, bool) {
Expand Down