feat(coderd): add coder_app usage stats (#9001)

Updates #8658
This commit is contained in:
Mathias Fredriksson
2023-08-16 15:22:00 +03:00
committed by GitHub
parent f3c707648c
commit 6fd9975aed
31 changed files with 1730 additions and 62 deletions
+7
View File
@@ -2046,6 +2046,13 @@ func (q *querier) InsertWorkspaceApp(ctx context.Context, arg database.InsertWor
return q.db.InsertWorkspaceApp(ctx, arg)
}
func (q *querier) InsertWorkspaceAppStats(ctx context.Context, arg database.InsertWorkspaceAppStatsParams) error {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
return err
}
return q.db.InsertWorkspaceAppStats(ctx, arg)
}
func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error {
w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
if err != nil {
+67 -27
View File
@@ -114,33 +114,35 @@ type data struct {
userLinks []database.UserLink
// New tables
workspaceAgentStats []database.WorkspaceAgentStat
auditLogs []database.AuditLog
files []database.File
gitAuthLinks []database.GitAuthLink
gitSSHKey []database.GitSSHKey
groupMembers []database.GroupMember
groups []database.Group
licenses []database.License
parameterSchemas []database.ParameterSchema
provisionerDaemons []database.ProvisionerDaemon
provisionerJobLogs []database.ProvisionerJobLog
provisionerJobs []database.ProvisionerJob
replicas []database.Replica
templateVersions []database.TemplateVersionTable
templateVersionParameters []database.TemplateVersionParameter
templateVersionVariables []database.TemplateVersionVariable
templates []database.TemplateTable
workspaceAgents []database.WorkspaceAgent
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
workspaceAgentLogs []database.WorkspaceAgentLog
workspaceApps []database.WorkspaceApp
workspaceBuilds []database.WorkspaceBuildTable
workspaceBuildParameters []database.WorkspaceBuildParameter
workspaceResourceMetadata []database.WorkspaceResourceMetadatum
workspaceResources []database.WorkspaceResource
workspaces []database.Workspace
workspaceProxies []database.WorkspaceProxy
workspaceAgentStats []database.WorkspaceAgentStat
auditLogs []database.AuditLog
files []database.File
gitAuthLinks []database.GitAuthLink
gitSSHKey []database.GitSSHKey
groupMembers []database.GroupMember
groups []database.Group
licenses []database.License
parameterSchemas []database.ParameterSchema
provisionerDaemons []database.ProvisionerDaemon
provisionerJobLogs []database.ProvisionerJobLog
provisionerJobs []database.ProvisionerJob
replicas []database.Replica
templateVersions []database.TemplateVersionTable
templateVersionParameters []database.TemplateVersionParameter
templateVersionVariables []database.TemplateVersionVariable
templates []database.TemplateTable
workspaceAgents []database.WorkspaceAgent
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
workspaceAgentLogs []database.WorkspaceAgentLog
workspaceApps []database.WorkspaceApp
workspaceAppStatsLastInsertID int64
workspaceAppStats []database.WorkspaceAppStat
workspaceBuilds []database.WorkspaceBuildTable
workspaceBuildParameters []database.WorkspaceBuildParameter
workspaceResourceMetadata []database.WorkspaceResourceMetadatum
workspaceResources []database.WorkspaceResource
workspaces []database.Workspace
workspaceProxies []database.WorkspaceProxy
// Locks is a map of lock names. Any keys within the map are currently
// locked.
locks map[int64]struct{}
@@ -4336,6 +4338,44 @@ func (q *FakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW
return workspaceApp, nil
}
func (q *FakeQuerier) InsertWorkspaceAppStats(_ context.Context, arg database.InsertWorkspaceAppStatsParams) error {
err := validateDatabaseType(arg)
if err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
InsertWorkspaceAppStatsLoop:
for i := 0; i < len(arg.UserID); i++ {
stat := database.WorkspaceAppStat{
ID: q.workspaceAppStatsLastInsertID + 1,
UserID: arg.UserID[i],
WorkspaceID: arg.WorkspaceID[i],
AgentID: arg.AgentID[i],
AccessMethod: arg.AccessMethod[i],
SlugOrPort: arg.SlugOrPort[i],
SessionID: arg.SessionID[i],
SessionStartedAt: arg.SessionStartedAt[i],
SessionEndedAt: arg.SessionEndedAt[i],
Requests: arg.Requests[i],
}
for j, s := range q.workspaceAppStats {
// Check unique constraint for upsert.
if s.UserID == stat.UserID && s.AgentID == stat.AgentID && s.SessionID == stat.SessionID {
q.workspaceAppStats[j].SessionEndedAt = stat.SessionEndedAt
q.workspaceAppStats[j].Requests = stat.Requests
continue InsertWorkspaceAppStatsLoop
}
}
q.workspaceAppStats = append(q.workspaceAppStats, stat)
q.workspaceAppStatsLastInsertID++
}
return nil
}
func (q *FakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
+7
View File
@@ -1264,6 +1264,13 @@ func (m metricsStore) InsertWorkspaceApp(ctx context.Context, arg database.Inser
return app, err
}
func (m metricsStore) InsertWorkspaceAppStats(ctx context.Context, arg database.InsertWorkspaceAppStatsParams) error {
start := time.Now()
r0 := m.s.InsertWorkspaceAppStats(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWorkspaceAppStats").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error {
start := time.Now()
err := m.s.InsertWorkspaceBuild(ctx, arg)
+14
View File
@@ -2657,6 +2657,20 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceApp(arg0, arg1 interface{}) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceApp", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceApp), arg0, arg1)
}
// InsertWorkspaceAppStats mocks base method.
func (m *MockStore) InsertWorkspaceAppStats(arg0 context.Context, arg1 database.InsertWorkspaceAppStatsParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWorkspaceAppStats", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// InsertWorkspaceAppStats indicates an expected call of InsertWorkspaceAppStats.
func (mr *MockStoreMockRecorder) InsertWorkspaceAppStats(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAppStats", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAppStats), arg0, arg1)
}
// InsertWorkspaceBuild mocks base method.
func (m *MockStore) InsertWorkspaceBuild(arg0 context.Context, arg1 database.InsertWorkspaceBuildParams) error {
m.ctrl.T.Helper()
+63
View File
@@ -812,6 +812,50 @@ COMMENT ON COLUMN workspace_agents.started_at IS 'The time the agent entered the
COMMENT ON COLUMN workspace_agents.ready_at IS 'The time the agent entered the ready or start_error lifecycle state';
CREATE TABLE workspace_app_stats (
id bigint NOT NULL,
user_id uuid NOT NULL,
workspace_id uuid NOT NULL,
agent_id uuid NOT NULL,
access_method text NOT NULL,
slug_or_port text NOT NULL,
session_id uuid NOT NULL,
session_started_at timestamp with time zone NOT NULL,
session_ended_at timestamp with time zone NOT NULL,
requests integer NOT NULL
);
COMMENT ON TABLE workspace_app_stats IS 'A record of workspace app usage statistics';
COMMENT ON COLUMN workspace_app_stats.id IS 'The ID of the record';
COMMENT ON COLUMN workspace_app_stats.user_id IS 'The user who used the workspace app';
COMMENT ON COLUMN workspace_app_stats.workspace_id IS 'The workspace that the workspace app was used in';
COMMENT ON COLUMN workspace_app_stats.agent_id IS 'The workspace agent that was used';
COMMENT ON COLUMN workspace_app_stats.access_method IS 'The method used to access the workspace app';
COMMENT ON COLUMN workspace_app_stats.slug_or_port IS 'The slug or port used to to identify the app';
COMMENT ON COLUMN workspace_app_stats.session_id IS 'The unique identifier for the session';
COMMENT ON COLUMN workspace_app_stats.session_started_at IS 'The time the session started';
COMMENT ON COLUMN workspace_app_stats.session_ended_at IS 'The time the session ended';
COMMENT ON COLUMN workspace_app_stats.requests IS 'The number of requests made during the session, a number larger than 1 indicates that multiple sessions were rolled up into one';
CREATE SEQUENCE workspace_app_stats_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE workspace_app_stats_id_seq OWNED BY workspace_app_stats.id;
CREATE TABLE workspace_apps (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -969,6 +1013,8 @@ ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provi
ALTER TABLE ONLY workspace_agent_logs ALTER COLUMN id SET DEFAULT nextval('workspace_agent_startup_logs_id_seq'::regclass);
ALTER TABLE ONLY workspace_app_stats ALTER COLUMN id SET DEFAULT nextval('workspace_app_stats_id_seq'::regclass);
ALTER TABLE ONLY workspace_proxies ALTER COLUMN region_id SET DEFAULT nextval('workspace_proxies_region_id_seq'::regclass);
ALTER TABLE ONLY workspace_resource_metadata ALTER COLUMN id SET DEFAULT nextval('workspace_resource_metadata_id_seq'::regclass);
@@ -1081,6 +1127,12 @@ ALTER TABLE ONLY workspace_agent_logs
ALTER TABLE ONLY workspace_agents
ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_app_stats
ADD CONSTRAINT workspace_app_stats_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_app_stats
ADD CONSTRAINT workspace_app_stats_user_id_agent_id_session_id_key UNIQUE (user_id, agent_id, session_id);
ALTER TABLE ONLY workspace_apps
ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
@@ -1167,6 +1219,8 @@ CREATE INDEX workspace_agents_auth_token_idx ON workspace_agents USING btree (au
CREATE INDEX workspace_agents_resource_id_idx ON workspace_agents USING btree (resource_id);
CREATE INDEX workspace_app_stats_workspace_id_idx ON workspace_app_stats USING btree (workspace_id);
CREATE UNIQUE INDEX workspace_proxies_lower_name_idx ON workspace_proxies USING btree (lower(name)) WHERE (deleted = false);
CREATE INDEX workspace_resources_job_id_idx ON workspace_resources USING btree (job_id);
@@ -1252,6 +1306,15 @@ ALTER TABLE ONLY workspace_agent_logs
ALTER TABLE ONLY workspace_agents
ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_app_stats
ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id);
ALTER TABLE ONLY workspace_app_stats
ADD CONSTRAINT workspace_app_stats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE ONLY workspace_app_stats
ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id);
ALTER TABLE ONLY workspace_apps
ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
@@ -0,0 +1 @@
DROP TABLE workspace_app_stats;
@@ -0,0 +1,32 @@
CREATE TABLE workspace_app_stats (
id BIGSERIAL PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users (id),
workspace_id uuid NOT NULL REFERENCES workspaces (id),
agent_id uuid NOT NULL REFERENCES workspace_agents (id),
access_method text NOT NULL,
slug_or_port text NOT NULL,
session_id uuid NOT NULL,
session_started_at timestamptz NOT NULL,
session_ended_at timestamptz NOT NULL,
requests integer NOT NULL,
-- Set a unique constraint to allow upserting the session_ended_at
-- and requests fields without risk of collisions.
UNIQUE(user_id, agent_id, session_id)
);
COMMENT ON TABLE workspace_app_stats IS 'A record of workspace app usage statistics';
COMMENT ON COLUMN workspace_app_stats.id IS 'The ID of the record';
COMMENT ON COLUMN workspace_app_stats.user_id IS 'The user who used the workspace app';
COMMENT ON COLUMN workspace_app_stats.workspace_id IS 'The workspace that the workspace app was used in';
COMMENT ON COLUMN workspace_app_stats.agent_id IS 'The workspace agent that was used';
COMMENT ON COLUMN workspace_app_stats.access_method IS 'The method used to access the workspace app';
COMMENT ON COLUMN workspace_app_stats.slug_or_port IS 'The slug or port used to to identify the app';
COMMENT ON COLUMN workspace_app_stats.session_id IS 'The unique identifier for the session';
COMMENT ON COLUMN workspace_app_stats.session_started_at IS 'The time the session started';
COMMENT ON COLUMN workspace_app_stats.session_ended_at IS 'The time the session ended';
COMMENT ON COLUMN workspace_app_stats.requests IS 'The number of requests made during the session, a number larger than 1 indicates that multiple sessions were rolled up into one';
-- Create index on workspace_id for joining/filtering by templates.
CREATE INDEX workspace_app_stats_workspace_id_idx ON workspace_app_stats (workspace_id);
@@ -0,0 +1,133 @@
INSERT INTO public.workspace_app_stats (
id,
user_id,
workspace_id,
agent_id,
access_method,
slug_or_port,
session_id,
session_started_at,
session_ended_at,
requests
)
VALUES
(
1498,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'path',
'code-server',
'562cbfb8-3d9a-4018-9c04-e8159d5aa43e',
'2023-08-14 20:15:00+00',
'2023-08-14 20:16:00+00',
1
),
(
59,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'terminal',
'',
'281919d0-5d99-48fb-8a93-2c3019010387',
'2023-08-14 14:15:40.085827+00',
'2023-08-14 14:17:41.295989+00',
1
),
(
58,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'path',
'code-server',
'5b7c9d43-19e6-4401-997b-c26de2c86c55',
'2023-08-14 14:15:34.620496+00',
'2023-08-14 23:58:37.158964+00',
1
),
(
57,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'path',
'code-server',
'fe546a68-0921-4a2b-bced-5dc5c5635576',
'2023-08-14 14:15:34.129002+00',
'2023-08-14 23:58:37.158901+00',
1
),
(
56,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'path',
'code-server',
'96e4e857-598c-4881-bc40-e13008b48bb0',
'2023-08-14 14:15:00+00',
'2023-08-14 14:16:00+00',
36
),
(
7,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'terminal',
'',
'95d22d41-0fde-447b-9743-0b8583edb60a',
'2023-08-14 13:00:28.732837+00',
'2023-08-14 13:09:23.990797+00',
1
),
(
4,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'path',
'code-server',
'442688ce-f9e7-46df-ba3d-623ef9a1d30d',
'2023-08-14 13:00:12.843977+00',
'2023-08-14 13:09:26.276696+00',
1
),
(
3,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'path',
'code-server',
'f963c4f0-55b7-4813-8b61-ea58536754db',
'2023-08-14 13:00:12.323196+00',
'2023-08-14 13:09:26.277073+00',
1
),
(
2,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'terminal',
'',
'5a034459-73e4-4642-91b8-80b0f718f29e',
'2023-08-14 13:00:00+00',
'2023-08-14 13:01:00+00',
4
),
(
1,
'30095c71-380b-457a-8995-97b8ee6e5307',
'3a9a1feb-e89d-457c-9d53-ac751b198ebe',
'7a1ce5f8-8d00-431c-ad1b-97a846512804',
'path',
'code-server',
'd7a0d8e1-069e-421d-b876-b5d0ddbcaf6d',
'2023-08-14 13:00:00+00',
'2023-08-14 13:01:00+00',
36
);
+24
View File
@@ -2016,6 +2016,30 @@ type WorkspaceApp struct {
External bool `db:"external" json:"external"`
}
// A record of workspace app usage statistics
type WorkspaceAppStat struct {
// The ID of the record
ID int64 `db:"id" json:"id"`
// The user who used the workspace app
UserID uuid.UUID `db:"user_id" json:"user_id"`
// The workspace that the workspace app was used in
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
// The workspace agent that was used
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
// The method used to access the workspace app
AccessMethod string `db:"access_method" json:"access_method"`
// The slug or port used to to identify the app
SlugOrPort string `db:"slug_or_port" json:"slug_or_port"`
// The unique identifier for the session
SessionID uuid.UUID `db:"session_id" json:"session_id"`
// The time the session started
SessionStartedAt time.Time `db:"session_started_at" json:"session_started_at"`
// The time the session ended
SessionEndedAt time.Time `db:"session_ended_at" json:"session_ended_at"`
// The number of requests made during the session, a number larger than 1 indicates that multiple sessions were rolled up into one
Requests int32 `db:"requests" json:"requests"`
}
// Joins in the username + avatar url of the initiated by user.
type WorkspaceBuild struct {
ID uuid.UUID `db:"id" json:"id"`
+1
View File
@@ -233,6 +233,7 @@ type sqlcQuerier interface {
InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error)
InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error
InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error)
InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error
InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
+66
View File
@@ -7831,6 +7831,72 @@ func (q *sqlQuerier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg Updat
return err
}
const insertWorkspaceAppStats = `-- name: InsertWorkspaceAppStats :exec
INSERT INTO
workspace_app_stats (
user_id,
workspace_id,
agent_id,
access_method,
slug_or_port,
session_id,
session_started_at,
session_ended_at,
requests
)
SELECT
unnest($1::uuid[]) AS user_id,
unnest($2::uuid[]) AS workspace_id,
unnest($3::uuid[]) AS agent_id,
unnest($4::text[]) AS access_method,
unnest($5::text[]) AS slug_or_port,
unnest($6::uuid[]) AS session_id,
unnest($7::timestamptz[]) AS session_started_at,
unnest($8::timestamptz[]) AS session_ended_at,
unnest($9::int[]) AS requests
ON CONFLICT
(user_id, agent_id, session_id)
DO
UPDATE SET
session_ended_at = EXCLUDED.session_ended_at,
requests = EXCLUDED.requests
WHERE
workspace_app_stats.user_id = EXCLUDED.user_id
AND workspace_app_stats.agent_id = EXCLUDED.agent_id
AND workspace_app_stats.session_id = EXCLUDED.session_id
-- Since stats are updated in place as time progresses, we only
-- want to update this row if it's fresh.
AND workspace_app_stats.session_ended_at <= EXCLUDED.session_ended_at
AND workspace_app_stats.requests <= EXCLUDED.requests
`
type InsertWorkspaceAppStatsParams struct {
UserID []uuid.UUID `db:"user_id" json:"user_id"`
WorkspaceID []uuid.UUID `db:"workspace_id" json:"workspace_id"`
AgentID []uuid.UUID `db:"agent_id" json:"agent_id"`
AccessMethod []string `db:"access_method" json:"access_method"`
SlugOrPort []string `db:"slug_or_port" json:"slug_or_port"`
SessionID []uuid.UUID `db:"session_id" json:"session_id"`
SessionStartedAt []time.Time `db:"session_started_at" json:"session_started_at"`
SessionEndedAt []time.Time `db:"session_ended_at" json:"session_ended_at"`
Requests []int32 `db:"requests" json:"requests"`
}
func (q *sqlQuerier) InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error {
_, err := q.db.ExecContext(ctx, insertWorkspaceAppStats,
pq.Array(arg.UserID),
pq.Array(arg.WorkspaceID),
pq.Array(arg.AgentID),
pq.Array(arg.AccessMethod),
pq.Array(arg.SlugOrPort),
pq.Array(arg.SessionID),
pq.Array(arg.SessionStartedAt),
pq.Array(arg.SessionEndedAt),
pq.Array(arg.Requests),
)
return err
}
const getWorkspaceBuildParameters = `-- name: GetWorkspaceBuildParameters :many
SELECT
workspace_build_id, name, value
@@ -0,0 +1,37 @@
-- name: InsertWorkspaceAppStats :exec
INSERT INTO
workspace_app_stats (
user_id,
workspace_id,
agent_id,
access_method,
slug_or_port,
session_id,
session_started_at,
session_ended_at,
requests
)
SELECT
unnest(@user_id::uuid[]) AS user_id,
unnest(@workspace_id::uuid[]) AS workspace_id,
unnest(@agent_id::uuid[]) AS agent_id,
unnest(@access_method::text[]) AS access_method,
unnest(@slug_or_port::text[]) AS slug_or_port,
unnest(@session_id::uuid[]) AS session_id,
unnest(@session_started_at::timestamptz[]) AS session_started_at,
unnest(@session_ended_at::timestamptz[]) AS session_ended_at,
unnest(@requests::int[]) AS requests
ON CONFLICT
(user_id, agent_id, session_id)
DO
UPDATE SET
session_ended_at = EXCLUDED.session_ended_at,
requests = EXCLUDED.requests
WHERE
workspace_app_stats.user_id = EXCLUDED.user_id
AND workspace_app_stats.agent_id = EXCLUDED.agent_id
AND workspace_app_stats.session_id = EXCLUDED.session_id
-- Since stats are updated in place as time progresses, we only
-- want to update this row if it's fresh.
AND workspace_app_stats.session_ended_at <= EXCLUDED.session_ended_at
AND workspace_app_stats.requests <= EXCLUDED.requests;
+1
View File
@@ -18,6 +18,7 @@ const (
UniqueTemplateVersionParametersTemplateVersionIDNameKey UniqueConstraint = "template_version_parameters_template_version_id_name_key" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name);
UniqueTemplateVersionVariablesTemplateVersionIDNameKey UniqueConstraint = "template_version_variables_template_version_id_name_key" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_name_key UNIQUE (template_version_id, name);
UniqueTemplateVersionsTemplateIDNameKey UniqueConstraint = "template_versions_template_id_name_key" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_name_key UNIQUE (template_id, name);
UniqueWorkspaceAppStatsUserIDAgentIDSessionIDKey UniqueConstraint = "workspace_app_stats_user_id_agent_id_session_id_key" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_agent_id_session_id_key UNIQUE (user_id, agent_id, session_id);
UniqueWorkspaceAppsAgentIDSlugIndex UniqueConstraint = "workspace_apps_agent_id_slug_idx" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
UniqueWorkspaceBuildParametersWorkspaceBuildIDNameKey UniqueConstraint = "workspace_build_parameters_workspace_build_id_name_key" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_name_key UNIQUE (workspace_build_id, name);
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);