feat: add endpoint for listing queue metrics (#615)

* wip: create openapi endpoint for queue metrics

* feat: add queue metrics api
This commit is contained in:
abelanger5
2024-06-19 14:43:01 -04:00
committed by GitHub
parent 8776b9f7f9
commit a9fa824d37
15 changed files with 1056 additions and 183 deletions
@@ -60,6 +60,10 @@ TenantInvite:
$ref: "./tenant.yaml#/TenantInvite"
TenantInviteList:
$ref: "./tenant.yaml#/TenantInviteList"
QueueMetrics:
$ref: "./tenant.yaml#/QueueMetrics"
TenantQueueMetrics:
$ref: "./tenant.yaml#/TenantQueueMetrics"
AcceptInviteRequest:
$ref: "./user.yaml#/AcceptInviteRequest"
RejectInviteRequest:
@@ -304,3 +304,30 @@ TenantInviteList:
$ref: "#/TenantInvite"
type: array
x-go-name: Rows
QueueMetrics:
type: object
properties:
numQueued:
type: integer
description: The number of items in the queue.
numRunning:
type: integer
description: The number of items running.
numPending:
type: integer
description: The number of items pending.
required:
- numQueued
- numRunning
- numPending
TenantQueueMetrics:
properties:
total:
$ref: "#/QueueMetrics"
description: The total queue metrics.
workflow:
type: object
additionalProperties:
$ref: "#/QueueMetrics"
+2
View File
@@ -96,6 +96,8 @@ paths:
$ref: "./paths/api-tokens/api_tokens.yaml#/withTenant"
/api/v1/api-tokens/{api-token}:
$ref: "./paths/api-tokens/api_tokens.yaml#/revoke"
/api/v1/tenants/{tenant}/queue-metrics:
$ref: "./paths/tenant/tenant.yaml#/getQueueMetrics"
/api/v1/tenants/{tenant}/events:
$ref: "./paths/event/event.yaml#/withTenant"
/api/v1/tenants/{tenant}/events/replay:
@@ -559,3 +559,63 @@ member:
summary: Delete a tenant member
tags:
- Tenant
getQueueMetrics:
get:
x-resources: ["tenant"]
description: Get the queue metrics for the tenant
operationId: tenant:get:queue-metrics
parameters:
- description: The tenant id
in: path
name: tenant
required: true
schema:
type: string
format: uuid
minLength: 36
maxLength: 36
- description: A list of workflow IDs to filter by
in: query
name: workflows
required: false
schema:
type: array
items:
$ref: "../../components/schemas/_index.yaml#/WorkflowID"
- description: A list of metadata key value pairs to filter by
in: query
name: additionalMetadata
example: ["key1:value1", "key2:value2"]
required: false
schema:
type: array
items:
type: string
responses:
"200":
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/TenantQueueMetrics"
description: Successfully retrieved the workflow version metrics
"400":
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/APIErrors"
description: A malformed or bad request
"403":
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/APIErrors"
description: Forbidden
"404":
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/APIErrors"
description: Not found
summary: Get workflow metrics
tags:
- Workflow
@@ -305,17 +305,17 @@ cancelWorkflowRuns:
responses:
"200":
content:
application/json:
schema:
type: object
properties:
workflowRunIds:
type: array
items:
type: string
format: uuid
minLength: 36
maxLength: 36
application/json:
schema:
type: object
properties:
workflowRunIds:
type: array
items:
type: string
format: uuid
minLength: 36
maxLength: 36
description: Successfully cancelled the workflow runs
"400":
content:
@@ -773,7 +773,7 @@ getMetrics:
format: uuid
minLength: 36
maxLength: 36
- description: A status of workflow runs to filter by
- description: A status of workflow run statuses to filter by
in: query
name: status
required: false
@@ -0,0 +1,67 @@
package tenants
import (
"fmt"
"strings"
"github.com/labstack/echo/v4"
"github.com/hatchet-dev/hatchet/api/v1/server/oas/apierrors"
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
"github.com/hatchet-dev/hatchet/pkg/repository"
"github.com/hatchet-dev/hatchet/pkg/repository/prisma/db"
)
func (t *TenantService) TenantGetQueueMetrics(ctx echo.Context, request gen.TenantGetQueueMetricsRequestObject) (gen.TenantGetQueueMetricsResponseObject, error) {
tenant := ctx.Get("tenant").(*db.TenantModel)
opts := repository.GetQueueMetricsOpts{}
if request.Params.AdditionalMetadata != nil {
additionalMetadata := make(map[string]interface{}, len(*request.Params.AdditionalMetadata))
for _, v := range *request.Params.AdditionalMetadata {
splitValue := strings.Split(fmt.Sprintf("%v", v), ":")
if len(splitValue) == 2 {
additionalMetadata[splitValue[0]] = splitValue[1]
} else {
return gen.TenantGetQueueMetrics400JSONResponse(apierrors.NewAPIErrors("Additional metadata filters must be in the format key:value.")), nil
}
}
opts.AdditionalMetadata = additionalMetadata
}
if request.Params.Workflows != nil {
opts.WorkflowIds = *request.Params.Workflows
}
metrics, err := t.config.APIRepository.Tenant().GetQueueMetrics(tenant.ID, &opts)
if err != nil {
return nil, err
}
respWorkflowMap := make(map[string]gen.QueueMetrics, len(metrics.ByWorkflowId))
for k, v := range metrics.ByWorkflowId {
respWorkflowMap[k] = gen.QueueMetrics{
NumPending: v.Pending,
NumQueued: v.PendingAssignment,
NumRunning: v.Running,
}
}
resp := gen.TenantQueueMetrics{
Total: &gen.QueueMetrics{
NumPending: metrics.Total.Pending,
NumQueued: metrics.Total.PendingAssignment,
NumRunning: metrics.Total.Running,
},
Workflow: &respWorkflowMap,
}
return gen.TenantGetQueueMetrics200JSONResponse(resp), nil
}
+303 -161
View File
@@ -510,6 +510,18 @@ type PullRequest struct {
// PullRequestState defines model for PullRequestState.
type PullRequestState string
// QueueMetrics defines model for QueueMetrics.
type QueueMetrics struct {
// NumPending The number of items pending.
NumPending int `json:"numPending"`
// NumQueued The number of items in the queue.
NumQueued int `json:"numQueued"`
// NumRunning The number of items running.
NumRunning int `json:"numRunning"`
}
// RejectInviteRequest defines model for RejectInviteRequest.
type RejectInviteRequest struct {
Invite string `json:"invite" validate:"required,uuid"`
@@ -729,6 +741,12 @@ type TenantMemberList struct {
// TenantMemberRole defines model for TenantMemberRole.
type TenantMemberRole string
// TenantQueueMetrics defines model for TenantQueueMetrics.
type TenantQueueMetrics struct {
Total *QueueMetrics `json:"total,omitempty"`
Workflow *map[string]QueueMetrics `json:"workflow,omitempty"`
}
// TenantResource defines model for TenantResource.
type TenantResource string
@@ -1156,6 +1174,15 @@ type EventListParams struct {
AdditionalMetadata *[]string `form:"additionalMetadata,omitempty" json:"additionalMetadata,omitempty"`
}
// TenantGetQueueMetricsParams defines parameters for TenantGetQueueMetrics.
type TenantGetQueueMetricsParams struct {
// Workflows A list of workflow IDs to filter by
Workflows *[]WorkflowID `form:"workflows,omitempty" json:"workflows,omitempty"`
// AdditionalMetadata A list of metadata key value pairs to filter by
AdditionalMetadata *[]string `form:"additionalMetadata,omitempty" json:"additionalMetadata,omitempty"`
}
// WorkflowRunListPullRequestsParams defines parameters for WorkflowRunListPullRequests.
type WorkflowRunListPullRequestsParams struct {
// State The pull request state
@@ -1209,7 +1236,7 @@ type WorkflowRunGetMetricsParams struct {
// WorkflowGetMetricsParams defines parameters for WorkflowGetMetrics.
type WorkflowGetMetricsParams struct {
// Status A status of workflow runs to filter by
// Status A status of workflow run statuses to filter by
Status *WorkflowRunStatus `form:"status,omitempty" json:"status,omitempty"`
// GroupKey A group key to filter metrics by
@@ -1401,6 +1428,9 @@ type ServerInterface interface {
// Delete a tenant member
// (DELETE /api/v1/tenants/{tenant}/members/{member})
TenantMemberDelete(ctx echo.Context, tenant openapi_types.UUID, member openapi_types.UUID) error
// Get workflow metrics
// (GET /api/v1/tenants/{tenant}/queue-metrics)
TenantGetQueueMetrics(ctx echo.Context, tenant openapi_types.UUID, params TenantGetQueueMetricsParams) error
// Create tenant alert email group
// (GET /api/v1/tenants/{tenant}/resource-policy)
TenantResourcePolicyGet(ctx echo.Context, tenant openapi_types.UUID) error
@@ -2342,6 +2372,42 @@ func (w *ServerInterfaceWrapper) TenantMemberDelete(ctx echo.Context) error {
return err
}
// TenantGetQueueMetrics converts echo context to params.
func (w *ServerInterfaceWrapper) TenantGetQueueMetrics(ctx echo.Context) error {
var err error
// ------------- Path parameter "tenant" -------------
var tenant openapi_types.UUID
err = runtime.BindStyledParameterWithLocation("simple", false, "tenant", runtime.ParamLocationPath, ctx.Param("tenant"), &tenant)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tenant: %s", err))
}
ctx.Set(BearerAuthScopes, []string{})
ctx.Set(CookieAuthScopes, []string{})
// Parameter object where we will unmarshal all parameters from the context
var params TenantGetQueueMetricsParams
// ------------- Optional query parameter "workflows" -------------
err = runtime.BindQueryParameter("form", true, false, "workflows", ctx.QueryParams(), &params.Workflows)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter workflows: %s", err))
}
// ------------- Optional query parameter "additionalMetadata" -------------
err = runtime.BindQueryParameter("form", true, false, "additionalMetadata", ctx.QueryParams(), &params.AdditionalMetadata)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter additionalMetadata: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.TenantGetQueueMetrics(ctx, tenant, params)
return err
}
// TenantResourcePolicyGet converts echo context to params.
func (w *ServerInterfaceWrapper) TenantResourcePolicyGet(ctx echo.Context) error {
var err error
@@ -3247,6 +3313,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.PATCH(baseURL+"/api/v1/tenants/:tenant/invites/:tenant-invite", wrapper.TenantInviteUpdate)
router.GET(baseURL+"/api/v1/tenants/:tenant/members", wrapper.TenantMemberList)
router.DELETE(baseURL+"/api/v1/tenants/:tenant/members/:member", wrapper.TenantMemberDelete)
router.GET(baseURL+"/api/v1/tenants/:tenant/queue-metrics", wrapper.TenantGetQueueMetrics)
router.GET(baseURL+"/api/v1/tenants/:tenant/resource-policy", wrapper.TenantResourcePolicyGet)
router.GET(baseURL+"/api/v1/tenants/:tenant/slack", wrapper.SlackWebhookList)
router.GET(baseURL+"/api/v1/tenants/:tenant/slack/start", wrapper.UserUpdateSlackOauthStart)
@@ -4613,6 +4680,51 @@ func (response TenantMemberDelete404JSONResponse) VisitTenantMemberDeleteRespons
return json.NewEncoder(w).Encode(response)
}
type TenantGetQueueMetricsRequestObject struct {
Tenant openapi_types.UUID `json:"tenant"`
Params TenantGetQueueMetricsParams
}
type TenantGetQueueMetricsResponseObject interface {
VisitTenantGetQueueMetricsResponse(w http.ResponseWriter) error
}
type TenantGetQueueMetrics200JSONResponse TenantQueueMetrics
func (response TenantGetQueueMetrics200JSONResponse) VisitTenantGetQueueMetricsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type TenantGetQueueMetrics400JSONResponse APIErrors
func (response TenantGetQueueMetrics400JSONResponse) VisitTenantGetQueueMetricsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
return json.NewEncoder(w).Encode(response)
}
type TenantGetQueueMetrics403JSONResponse APIErrors
func (response TenantGetQueueMetrics403JSONResponse) VisitTenantGetQueueMetricsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(403)
return json.NewEncoder(w).Encode(response)
}
type TenantGetQueueMetrics404JSONResponse APIErrors
func (response TenantGetQueueMetrics404JSONResponse) VisitTenantGetQueueMetricsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(404)
return json.NewEncoder(w).Encode(response)
}
type TenantResourcePolicyGetRequestObject struct {
Tenant openapi_types.UUID `json:"tenant"`
}
@@ -6141,6 +6253,8 @@ type StrictServerInterface interface {
TenantMemberDelete(ctx echo.Context, request TenantMemberDeleteRequestObject) (TenantMemberDeleteResponseObject, error)
TenantGetQueueMetrics(ctx echo.Context, request TenantGetQueueMetricsRequestObject) (TenantGetQueueMetricsResponseObject, error)
TenantResourcePolicyGet(ctx echo.Context, request TenantResourcePolicyGetRequestObject) (TenantResourcePolicyGetResponseObject, error)
SlackWebhookList(ctx echo.Context, request SlackWebhookListRequestObject) (SlackWebhookListResponseObject, error)
@@ -7182,6 +7296,32 @@ func (sh *strictHandler) TenantMemberDelete(ctx echo.Context, tenant openapi_typ
return nil
}
// TenantGetQueueMetrics operation middleware
func (sh *strictHandler) TenantGetQueueMetrics(ctx echo.Context, tenant openapi_types.UUID, params TenantGetQueueMetricsParams) error {
var request TenantGetQueueMetricsRequestObject
request.Tenant = tenant
request.Params = params
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.TenantGetQueueMetrics(ctx, request.(TenantGetQueueMetricsRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "TenantGetQueueMetrics")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(TenantGetQueueMetricsResponseObject); ok {
return validResponse.VisitTenantGetQueueMetricsResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// TenantResourcePolicyGet operation middleware
func (sh *strictHandler) TenantResourcePolicyGet(ctx echo.Context, tenant openapi_types.UUID) error {
var request TenantResourcePolicyGetRequestObject
@@ -8226,166 +8366,168 @@ func (sh *strictHandler) WorkflowVersionGetDefinition(ctx echo.Context, workflow
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/+x9bVPjuNLoX3H53qp7TlUgAzOzZw9VzwcGMrN5lgFuAjt17hZFCVtJtDi2V1KAPFP8",
"91t6s2VbsuWQhDDjL7tMrJdWq9/U6m5994NkniYxjCnxj777JJjBOeB/Hl8OBxgnmP2d4iSFmCLIvwRJ",
"CNn/Q0gCjFKKktg/8oEXLAhN5t5vgAYzSD3Ienu8cc+HT2CeRtA/Ovjw7l3PnyR4Dqh/5C9QTH/54Pd8",
"ukyhf+SjmMIpxP5zrzh8dTbt394kwR6dISLm1Kfzj/OGD1DCNIeEgCnMZyUUo3jKJ00Cchuh+N40Jfvd",
"o4lHZ9ALk2AxhzEFBgB6Hpp4iHrwCRFKCuBMEZ0t7vaDZN6fCTzthfBB/W2CaIJgFFahYTDwTx6dAapN",
"7iHiAUKSAAEKQ+8R0RmHB6RphAJwFxW2w4/B3ICI556P4d8LhGHoH/1ZmPoma5zc/QUDymBUtEKqxAKz",
"3xGFc/7H/8Zw4h/5/6uf015fEl4/o7rnbBqAMVhWQJLjWqD5CimowgIWdOYAAOt8zJo+9/w0IXSWTB17",
"XcrWrOMySuLjNB1a9u6SfWeb4g1POfksCOR9GG14AdtTskjTBNPCdh0cvv/w8Zd//brH/ij9h/3+73cH",
"h8bttGHpWOKkiCm+LvFnFXQJFww9NijxkonHMAtjigLODjrEf/p3gKDA7/nTJJlGkO1YRgkVYq9suQ3s",
"IZMTGCjhUKK5mJG5AfGPM0hnULIqyodgPCM7eUnM+RvFhII40HjjLkkiCGIGBGcaI27YF4YQMUQOY1UG",
"NDKd5Ey1mBpKv8yJtETwKfotIdRCgQmhvyVT7/hy6M1YKx3GGaUpOer3Jf3vyy+MOE1CCqTod7hsnuce",
"LgvTpLP725x0wV0Qwokz+Y4gSRY4gGZmDzBkAvDYsnqK5lATnViO5T0C4smuBUgP3x0e7h0c7h28vzp8",
"d/Tul6MPv+7/+uuv/8/XlFkIKNxjA5tQhCyCAIWCXjQgeh6KvetrIRjY0Dogd3eHBx9+ffevvcMPv8C9",
"D+/Bxz1w+DHc+3Dwr18OwoNgMvk31IFaLBBbyRw8ncF4ypj9/S89f45i/Z8VaBdpuCr2IkCoJ/uvE4Ul",
"9uCryjdZB9nCKlfJPTRJi6cUYUhMS/02g0IaMNqlrLsnW+877/scUhACQaENKqRA0FYxc1USMxls+8Vt",
"Pvz4sQmHGWy9TNpkyDAiMQhgSofxA6JwBP9eQCFbivhE/LPAbEuibUOkPf9pLwEp2mMW5hTGe/CJYrBH",
"wZRD8QAixPbFP8pW3OOs8FwhJAGvab0nnLwU6VhXbN6nY7FLwjR80Tbx8V3gI2kSE1gFkCrKr1JSAax6",
"MMQodjguF1EkcfQZJ/MxheloYWC4OwziYHYukVY/p9b2JptofD7W9L91W2iSouAY2xY+B/+TxJ7iOY/N",
"4f3jeHT+T8VY4/Oxx8eoomYF4puj+L8OenPw9F+HH3+pUmEGrB2/VzAGMT2OIKaDOUDRF5wsUuvqIWtC",
"TGQZIULZGkULZXxidlBxtMxWWH6IHmCPz1hduwS1aeUNckcMbtxr/kltK1srO8UJvl/L3qp19XycRLBJ",
"0ovVfIXzO4hHrL0RH74crAkrLaVSWXtQPsg6sMCXQaLF1Dwp+7L+SXvy5MwlxLPFhOZAmfA4eICxAXMg",
"DBGDHERfNeVdYqOsjae0aCbqIRtWW10+4b3JSGbYuYdLa/e12BIC5W6kmbc3nV4ZuMPT4laWPQ7SH2Fd",
"yGOC7ydR8jhaxOPFfA7wsgkyvlXfqt1qTBpx2MgWcqM2/BSYzgvmbWaLre6t94//Hl+ce3dLCsk/mxUn",
"Hzqb/veX0YAa4wyZmD4FUxRnx+I6hF5mLTO7gcuvR3dnTbac6sldAborUNaAeIFDiD8tTxGGgQIJxos5",
"2zlAAl94IjX5UdoL2f+z8tOpvvnZxNp1DAEOZkZ9a6P3Ci4nABl9HVzQL5iOYawqWnl4ERePLnb3awrj",
"kMHSMLBs1mbkvxdw0QyxaNVmXLyIYweIZbM2I5NFEEAYNgOdNXQfndHhF0iltXyKJhO7HR+iycSd8LUh",
"G92pYmQmo75w79Rxmg5jQkEUWXxsIAiSRUxvwQOgAN8ucGQkY9UsNlv7PR9ps9wSSCmKp8Q63MoK0K4l",
"7ACUoO+Z1myyKgQGP/GTi+30U4MQchvCCVhEVPuc+R6NxyMFn9bVDtcIpkkVKgzTxA4T/5o8xhA3n9i0",
"tj1tWBNA/53cGWi87tqHq2Pt4kcaIX8ld/sb8r1UxiQUpu14sMp8RfOqetZCc5gsqHn58mPT0h8gJiiJ",
"jTPYmSEDSx8gcw6JpVt20njUD0AcwChSjkQ3j1nWKbt/tDcZQUAEoRguzmJEZu2m/ktQZN2OMqIVLS27",
"9wKiw5AU+T7HMKEA03aLIRTQBXFYDzMvRFtJ36NF3FrNrEDlwT3E9SzQZrnamaIJZM2uKvVcnV+KgygC",
"yXbBzjXjbJuU5Xg5OD8dnn/xe/7o+vxc/DW+PjkZDE4Hp37P/3w8PON/nByfnwzO2N8mE/MMxfe5zCeI",
"Jnhp9RZMEWWtcq1VlTw4G8UTescoeORA51bvgzYMkyt1g1wolVM7Clc2xmF03W47yhrAaXVXUvEmF6Ys",
"4qO0sF4J6yYaYQco852n6316uauBT+Uk3ItM7ObnVo9t2YWN8eTGIDZaqrsCvtmMbjLDNRDlfDaa0I1M",
"WFh0C/Ak3VkoQpMdK47PbU3L6NptQd2eaa2cJ9eGbsa4PsGNhK14wUBemZSK0KyRhsYRCO6/wbtZkty/",
"+iI1WNa1xGR6hmLYKjSAaQT+mZ0umLmh7OwomXoRimGbi18RZWacgw0nGzSeXGy9RQuDn7mELf2SPA99",
"y2a4yVF1Bh9gpFsip4NP18z6GJ5/vvB7/rfj0bnf8wej0cXIbHJo42QOMaf9L0BgEhny++v7ExVZmfWS",
"+PgCn2JxhJZeRdm5xq9oQIB+U//dDxYYw5jeppx2D3t+DJ/Uv973/Hgx5/8g/tHBu+dembMKnU2RI7KF",
"lwoqzCY+dHLEabAYo6/gU3Xk924j5+syBrwkFES625M15d76CBEq7q7yGNd3Ln4/g8TSFVedKvwECMwt",
"9coeay1/gyB0azk81VrofuC8yTlffmMzdqCBLXS0aF8c4wrRyO6LEvb6eZ27SjS5cPdZ6R0qs5QxZYDV",
"hCnbVvQsm2lA402RLDLcKnmQpJApwSBKSCFIMMfGCDLy+nmCdkYwjcCS35/YYwXY12FYFPrbjrGrj29W",
"EN7wJeFFLP0sNVuYLky+owrmWDM2asmuNAw4hYReY0tQxfXozKOJR2Ac8lAZaVoQjyabuby2neAXMfp7",
"AT0UwpiiCYI4u0WVl9MyUFFE9Ohhv3cwSuKpgri8ndUN21xAkZuPqTZIqGBBV23eGYhjGNlwKD97KDRf",
"5RM2uPcoRjdurxzB7vdRU/DokxUneRENgblt9ezbC5bOutvXzQd/yaJ3gvrd6FMhIkN3kS56GhkaSZjC",
"1HTVqOSTgehQFGJY9Ic1CNoN+e5TgFUWlTskGIIQ3EXQ7pwU37MocegRCtNGMnnRlZJlBjsFaKsokINy",
"gcsNFOZ7zdZv4ArpmA7SpGD8ajbrmi6aOBFqVwotaWA1GoZWkFe598r71KCrbGAUrs0cbl3kJWHWfv08",
"mCyoDcQV2ZNHoRxPqDhFuCFz7bd4okvNzrjd9EkWK171uV5gs7Y22eIgeNqsOOtSs+LHBNsuD500VUaB",
"2cpqb+r0UJqKhJJBnVVCTkKmkM14STCaohhEzQsQYYxZe23cmxwySyQrj1cxY1CxWGWtKDR30PyZBpJX",
"0tGBAOXRjPdglAUfIEZ02ab3WPXJL6xr6O8zwoSOoRCx7jR4Btr1MiUoFacvjavDnuFQQ4jurhU7WUOb",
"uxJuWSBIo4/UQAaaQ2M0+L/Xg+vB6e35xe23i9Hvg5Hfy38cHV8Nbs+GX4dXfs8fn/w2OL0+G55/ub0a",
"fh2c3l5cs5+Px+Phl3N+QT6+Oh5diTvz4flw/Fvx+nw0uBr9R1yv5zfpPZ+NdXF9dTsafB4NZJ/RQBtV",
"n2x8dsFang2Ox9mYw8Hp7af/3F6PB2YvuZGSNRRo3nYJ3Wh4NTw5PqsbrS6MQP51K9bwdXBeQtOLwgyu",
"ssjyktkeQSyzHAaWXJRvKh838XhrdWSZ815k35h8C2IQLSkKyEVKLxa0ZtT8DDQDxEtSdvCSdm42iHmO",
"jSft2TIgXpxC0ZziZ82GMOYXbTexaDMhnjX5RcY174AUNe+FKQ9rmuwJkvNHbAIuYbXeKJ6OZVDr9lhU",
"5KwPnlLEdpmHcXBg6scXvcQ0xHvkubc8IsUDGHogTXECghmKpyIJlyO4bn6VHyWI5AzNEV0RCrFkleVc",
"hSdiY9fiQjsefgYoWmDoAAp3reqAqBgzHl3OY/rNc0aAiKXaL7l5ljZP4wax3NlHQNiUtMUVN3hSRPaZ",
"n5XiwJLbMgdP3kQ18QD1HmcomGVUtV5nn10SGAG2y4VhdiuymVTD5yzh2u6Y0dLsxTBbTUFfLZ+xyWcp",
"GcrmcVWf7VgTLep8rnyEQgL2ChqzkIiZ75WeVNZAOzujSiQpt9MgYk+r8L8aQbnnLzLWa2p9TSAWPS4X",
"dxEK6kiBj1eTkqvDvDObLvdvlU0fyX1Sh4mLb+f8JHZ8+nV47vf8r4OvnyzHm6Le1QfJznODP8QZhP3w",
"+ezi2+3o+pyfcy7O83PdwGF0rtVNhg3A8z9AtLDIB/7de2ANzCKIa3Qmuh8B5mkmFXUveu8bg0mYch3B",
"CYqiJuXLQ8v4cEz7Yt4Hhu6infetWagY275EM/wvS2HItr2ZQDMiee75D/ZVqKChpg0zr+YRxWHyaLFL",
"IIXYEy0yTSHG8v6B9uG+d+CFYNnzDrxHCO/Z/+dJTGf/XPGGLENPYevU4u2CRSHqMolQYMgCFRZo3aEs",
"q9EjmhrUYgvBUmS/plgGCZxxdRhNpxBr5rE1vsGclV91X7YNg7jmdYN+xuIa+sob4oPWUtfCqjx1QOz7",
"/4b9Sd2B+HUPxBs8qLqzazJHNEZRL1yoSP6XuwtNUaTXxGSzN55ZQRhiSIh+di3YIeowVD3Csg+/ATIz",
"ycMZIDN9yP9DStNJCSlUuaiKORYFJr2TGaDWCf+AOLtds/MrP4Ezbn2QzdmvCBdhMNPMDJBLQMhjgl3n",
"AF4qO3gE0i16lkNE0ggsCySj9q/1YbeI3RsLgZ3MQDyFCkH2IkTw0Y5ETuXwMceasknMsK+gGNXIfN1p",
"LSAZELX4exkMlTQW+aVXwJMN5WfJFMWrl79ajb9fVA1r5zCu1pg24XoEp4hQ9v83hG43XWIRDDu4W6om",
"peum6QYomaGUvFVHTMUxtUVtvgktIyYzbds3Hsdji/m0uMXlR3HwF5FAXgBiL4WYrW+/1X0jeAAoYhap",
"itxrqB1UnRY+wWBBoRcksXRUREuzJ4JhkNerw8OGasDcy4GmMQy9vNMO1AWOAE9kwfQOAnpMa68tcixx",
"fxeBMfWAN1O999dbX5nNwfgdxhAPCAV3EY9o3CEI5+DJTmNz8ITmi/n6aG3jIQwCqn1zBGQAY1XSithC",
"m1kbHmwsT2/qDYV84JfWH8njIg3BFPxbdTHKV318cjX8Y8BzX+WfN21jLewCbwcUk5S8xggtdTg3VWdK",
"o2Q5h81XMWqM06zHSRJP0LTxaQ9LjSd11N+31O2xbDP7YhrCCUey1k+ZrLh7v3WVma0wpBVDygIzyEIw",
"XR1Dao1XwKhcZRGpdlTJxvtDdMySd+rcy65sx8Y9UXLT5ESfQqp9z8KfSi6NWNof0jM0hZRw3AV5V2/K",
"+mbHWY0Q9q2XOGOKAYXTpU1aia8eTYS3RFWm12cVlz28Hj8IZuIaSYkzEcN3Ozy/vRxdfBkNxmO/55+O",
"Li5vzwffBuMrv+fzEMv8n19GF9eXt6OL6/PT29HFp+G58TqupVLL9VbRd1culfj+0JwyXdh3OXUZgT3j",
"RtZRRUVG/Rzlkaa2Uo8rFbYxjmaLAMntXDGed5ymnl47ySkhcwMRQi3KNdmXfKPRlsirL7tFM+Ifnhq3",
"RvU2GwovSlPbso3B7Qinp31U66+QYhQYwhW5VP0dLk9UVoNBmZXqNFSF8j1cErPFrIZnQqVmipKFzoQ8",
"8EgKAzRBQT6J948UEAJD7wEBb4IiCvE/HctAfCtWsnO5Bs1/vdRaU7yAhvHlQd5aP2G9uWXZhWwrGEWC",
"jjup5cllawsoVkljQoZt+6Qt5h7ryS3bBmFjNSf1utZZNlp9GpmIE4Dhp2WLwa+0XlqdR2lctrRFDSO8",
"vFrkH1qVVYm74mJv6uXDjhwktWOOu5xfV+1Labmas1MqkymMtV2bRqglAWMhOEPZsSS+1CRKNZEYJ/GY",
"Ge4LS+0b+OBw8M4yr2QJkc0kurck/axTAz2TE55wbb12KVRZLdLoJquxlKZtWoTVguFZfG2oTg11Ijo2",
"8VOpeWV+yT3G/E7FecaPksOM3xSjGj/mvGv4XLeaKzA14S9K8HpcRi/2qZgvqwSEdQQiZcQJZjJ3YhYT",
"NSnst8jCeU0TynzLiaUa0q0tjfmF0xLzCtvrlxLeTDURHiop/i0GzvCzXhtSKH0z+nI74FZ6ytqjWTNm",
"yrxScHW5YEL3jmle1Zf4Sl9SzQGHpbILNsdQjz9Yy9TmVV7nxGZDtqULojkyzQJDfnQSO4+aa9316Kz6",
"tNS5CmaFycJAN80kdQrZcc9cGwyDx+JngwsKPHr/Of565oVZw/ZStTiPA9Dmx2C3RIU/AZXwgg3BAiO6",
"HOePRN9BgCFWb0mL16PZMZX/nC9wRilPYg2S5B5B1RwxDImflAv/yK+8iC6fGOb3UyieJGYkq8fnjy+H",
"vNoCryLpF3/Ndsk/2H+3/45vcgpjkCL/yH+/f7D/jhusdMaX1gcp6kfoAcobguq8X9QNAGsVQ0K87CTH",
"aDDzg/pn8vsXvi4sD1x8lsN376oD/wZBRGdcjH40fT9PaDZnYWf8oz9vej5RT0YxCPOG6i7oTzl+MIPB",
"vX/D+vO1YgjCZfNiWTNUt9qRarDO5XLgeDQuf4rWoxhMJjJzq271GbSNy3846AMZKrzH41b2uEuP9L/z",
"n/XfngWMEaQGS/KU/048kD2RxyPSRXQO717BWCn7QIwgDk+AJ6owsGvSHSszePwgxPmL0XPOXZWl+Lp8",
"EC45IWNefLJ6vqns/YcqtsaLIICETBZRtPQESsPC+4IV5D33/A+CSoIkpvJcDNI0ko/Q9/+ShUXydTRI",
"fl4FTEZglf31cxAxLMDQS7B3B0IP5/XlP7x7v3YwTFB8TvAdCkMoAsdz+hZ0UkdmiuJleuRNz3/ay4L3",
"+euz4kPPQBg3/AhATbdeIm3jJSQuRvgxSFwU802E7FwLMThkJhnIpBZbNJFPs1ew8WwW0WtZiKWaRRX2",
"ghiQD7p3YsBNDAhq2ZwY0BVkivZEJlL/e/Y314ZpQgxGwwg+JPe80kT+kL64N8tmLImJFPEkKbEm0d1F",
"SmTDW2SCgnWn1B3my5N0rp68+YGJmrShakk6bGOv5M4pMs5/q6PkbMsLFCxcRf3v/P/PfXU2tJm8fG+y",
"d4BBnL/PW6Tb7H1hYfM20qt4TdimwoS7f5ukuj6ay19abhLzGFKM4INkAIERvh8dFxSOMBpmch4Qlz01",
"9C9oqED7ImplD6RpX4+4IVYGOEOE2uJ0que+LECIdRuWmm6M3hweBGtHiMVF7hItHmwHjOsYLOgsweh/",
"YCgm/ridib9COktCL06YFRMljzAsH++/FzxIf948F877TeSqeEc0ceON/vfpbE//5bnPQ+yceSYLyEOw",
"gWX4g2suykMHx6pDSmC/UW1ie46uHUsX9qDj6LfL0SVmKjN0RRuWmeBFLM9/Z3/t8cja5/zfjOWe+3fy",
"TUZn0ZB1qBULn/JWb00y9FwilK1A5qiuBbHtpOrNdPucsoX7lNuRgJU3P9sJwYzaOgH4dgWgJjLWIfz6",
"j9qzQUYPjjb3NEruQKRerLEILeG4+cKbfstaNt8BFQg3xQn7BwyzyTqa3SWaLd6yCQoBJgpptrgVBfa/",
"yz+enWhROjtdaFE4PXNabFSiclCr/nzUyHqrFnXHMT8cx1TouI5j5rDeWUmyt4GzDEQVAMEVQczLDxY5",
"ReWS2O/q14U+mYDVxmTJolV2hZgbgg30kH+5j1muDtfPqvNekoqEGHkFV9rkPio9Qm4/ToAo8gqtbRss",
"nHKFhhu1WeWOFx4wb7X5qqBiYXW7RAhFI620CdX91zeZv/3X/87/5xBM4o31twIrW6w/AOkeO1IY06rs",
"OIg7GSRSxEmn7l5f3ZUDU8pUq3iC/153dyGIrsgxMel/JzFx4pbie5dVfolJCzYpPZ5pZRQpUneOTUrI",
"6BhlBxmlQrAZq5yPaxklJgY2EZ+f1SW3/UDF5lV3zRUWaR2iZeOMDNqtuhnFral4Q2+lK3YNhsOPHwtA",
"HHRHtu7I5nRkIxSme3jBlZf887kfYAgo3EuxnTNPeBMPeOkiitTOyNCpLEegwrQiZ1wwrhjhErswsKrF",
"ZVduEva3F0op0bCIVGrpZ5zMs8ph5iBKXrjAo4kXmHZhqwGUbcEvSBgBvjhNFVbwcwfYsFk/bGfW84Qx",
"7SIu633J3iWyUoIkS+6p0/yKI5vFTSgfr62PcUOTiSppoqTBHaSPUJabmieEqtJ97BuIBV1NECb8l32b",
"OPoCKX8+9y3JoQ1x8xdItQeFV7zH49vZcfArczDjm1CQ9YbYNk9qtnv/RJsS39oYkfUYiEHfCC/2asow",
"0cQj9yhVsP29gHiZA5dMJoR7tA2goJj+8sFYkal+OlFl725pmZJ/bjnjJoVN5XnoFeJxSSdotiVoCjz3",
"CAg/mpgkT5nxNbbPvAbsJ2YhrkcSRcm0Xg4RL0qmXoRiWJZFVavgLJmeoVi8j9iJod0QQz37c1YRfIAR",
"YfOKUnY1E/OWhZlr74gkHbBenxGMQtvKCQQ4mHl8Ng2OSYItgIgObQEZi14GIL7xd1gTj6fJ29fPP39a",
"irW0nPxC72vBg5g+RBgG0klYA8Wp1mwVSPL+G45u06RBk35iJNkpJ0uiCNcKmRTWdMFZMm2vBsRn0uSf",
"Ih7g7/hYkvlE5I1o6m/SvVN8QK4+NTZz67xCImwrjw3NnrTt8lx1p0lGbE1ZrSaKzi5HRLXEmux2nuz3",
"hAhF8bSewN/ORckW0tXdmDAv0vOqiekdP64t77xFlnktX5prsNRHvYHMWrXlwJOmehSux5Gd4OBtFmtY",
"wXNg34SOdwrmWh21ujNTr4WJ1r5QS2a9/azKTbcw11eLxdkEPXjlWixVDdjVYnG1UV9Ui8VNS/YJpOz/",
"pLlum+riqS71lVg0ckHxdCz7OJa3+EnUpIaYF+hIfU86VircsFnRtDY+ygoa1V+0ZfWFiFv9os6ezLIA",
"OD5WTVpVsU6dr69sPGZFkEi7ykhNBuMKxbo6G5EjQNG6ZhZuPjQsn7Tjr3Xxl2SEFUuP1Ssch6gOwpO6",
"CqEdorel7Nhb0TU/8zXqPVw6XaKydoVZnUrwczLgRbKrz5nYYdKeonOCLZcVrQHU3sRbDUS8iGW5aegE",
"q2rrfP1pfi7ola6k+X6+zoU0n3oHrqN1OPTL6BpiyZKf7+HSewDRAnopQLhCL9lran8ydjs44k0P/B77",
"16H41yET76b1GB7hMzJD0ytDG6/A2N2or8XKhipe0rHuoovy73Mh72gBCM3hYAX8DpfdoTNXhSvRP0d2",
"xwMmHvCkZbJOPsAwjcCyrno0+67f74mOFg5QNaP5oD/vYVQgQD5qVXtBoYoQcw2JFd62dwnfJjSZAdep",
"qkJk8uG/tzOrehhJHpngUwBhWEllFmS3ZoWJ4gdEYdvrf9XLfKUx5F87XaluMjR8rHSHobDd3VyYLvdz",
"WtzQjb6YoJbWO/+sdocvUOJ2dS9w+6r39QLcVa7pJWF0bGm+m8/4Zj0XiZLP1Q974t/tHgpzYOXWT4Pt",
"lre3yFf1sO1l6HjrurWRew3vnu0Y95qq1mT7Y8stKO5jm/fEHDjhjZen2UFO2Gxg+Gp699VCwx051/BU",
"2S5zrgzZbs25dZpvDud38nHlFmc01cvM4l/51+6MpqhRw8dKZzSF7c4YNJ3Rclpcjy0ox+t/F3+4lCwE",
"EghvgpN5U1CmoIYfwxSUy7bBJj5vv7Di2nl3FRvw5+DaN1DsJWPSwsasTV6oHntpEqHA6Qlu6ZEVHVwC",
"uZUT95L36MK4+ya0rKZcS7vRKdmtZ0OIesG1AdyFWsSktoR2Z3aK2G0dJ23yG0qo7qqa7lDBYY0XLAXr",
"G4pzOzBin1CAqZUdx+yr0GMXxws684y1TK8JxOK0ygG6YAjlPd8iZ75/d9hQDJijTKqVAlZmEITydB0l",
"gmCKtFKe+7nV+1IcpcUZFSGwHViZDpryaUoVr4mpAHUnh6UcPh8XHu5oIYnLWO5k8c7J4iojONV+b0zj",
"cXgEobsX5ggo8ldt9s76aLY4qfP9bveaww4ztJXzHDm6VqMaCjPWFnXW6zgvBefayjO/WR/ij14v2rXQ",
"e9EtkRUE7fyGu1a7lTHmWuu1OsmJfgDiAEb2QOtjSuE8pTwOWLR1KCctn5kQQ3cS5G1LkBARHpshRYgg",
"gmj3bIxXzodoYpRtMTSGrGNN4gTr4MzDvHnHwruYyoEXsdyqhsiZ7M0acT9kWu7zTlgqXSJHjXzhG/4a",
"AiVfU+0rMaKZ43sTXyAdi2E70fJ61oEcL7n7CwZ0xZOE3PfuQLHTBwq1SxuRGo8Jvoe43uMQRZ5o1lAk",
"6xtv1Ln3RdWJleL6uro9lopxkgBLzzKtfpOlEC01pv7PJv9boXhJI0PI4iNv2R1XWLANNB2Db5hr5Xat",
"yLade87MuRlu2r2qVqCp1fm5n+KGa2um4vRHG0npSVajOayRCxtEe8yTdKy+KQAL7+YSKu45bZWioHNZ",
"Im3zxrzj5q//dXpZscqfyr0okG4nf0pX8UXsbFwCESdjmrd0sx46gzpXzZ1JvX7F3I4nHJmg8W7qRLnZ",
"7wANZuWigKRO276du6kN+Y81XBCBDFcvsrzcKNusZO1O5fwlIPavxxzgYUgKFfVehOBqzcmWfjF5IWYw",
"4ztxoUfACLIpk0xZatT7pFwlBx/cVYdyUJxP4V1J3V0tqavXLWNzTiHNtnbfMjFvPwz9bZ2J3CFTXTYP",
"XAowQ5rl3FYCSzT+pgvjLcFnuHExwiavmDYL107XIe7q3tag7gXmf6fTG3xzmzkGcHfcnO1IQJriWgVo",
"jPSLjCm7t/G1f5UzvlVd36nCThVuVxV22qZ6tlZS5AUaRwmvTvNsRPMsCMSkHywwlkupL7AgG3qsmzE1",
"9QukJ3KwDdIYT8FsR1Qc4i4r6PWzglwzcBmRl8itmIFbJeMporPF3R5I034AouiurvjBScKkMIUtEq6/",
"8OGP05QnXZ+oGVrnNAdy6u1nNatF1yU216J1vRnsRYSqLPYfJUPchsgN0uYOE+ba6HAjNLibBPhiekuS",
"aQQ3Q2986B+c3gT61kxvOeJ+OHprepUgf8KnWASeOyaczEs2gl6HlPi79AyA9mzOT/UGgMvhxVWtur0R",
"YKW9PggCmNKalE7+vV1JZdHH38xdtBi8UgXYcn9cQ31i5V2t+/pERY6kxlr3dvrCkN+L16QYsu/t6Ev0",
"8TeVK8cGXwN9iZV39NWQqMaQtAJ9RckU1WSuniVT4qHYA1w37tcYGGd8oA3VLWcqmI2/paecnfw8UTKd",
"wtBDXdGX3XLvFNU6oxpXP06UTJMFbWCGZEHduIENtSM0ykDpiPTt+CAF9biSrSxLPUNpiyOQ1sntGKQX",
"z+bdZEzWRgncPGn785COou5MtMqZSMdgM0mmgJDHBId2WSpfvhCS1FPt60TqpRpzczbGyQzE02yiXTI2",
"Ag5ZmCGqE+dvSJwLsipSugMTYThlggzXHfpEC1JrkZzob+Vtgm0UGLvEMAp53TXsm7DTFQm52jyisPUm",
"bhjy8tY/w02rTM8XibfyaZyGFHqDbShS+h1T5cUYtUmqW35hZt31I1aIO9ox6bQzhSNa1I3oKdKpELiI",
"aM1yy93ef8oCnZyiV90ff9JCMeuztLf/yJLja0gZgF35od14B0mjmFVypHntcpe6KU6c0EIL7B4brD8c",
"dcUY1E4bmMNOVyfxBp3Qj1B8vydCfWo8wCi+94AnmnkYpglBNMFLjyYao1h5Q/qGUXwvwn/eFKOs/+yY",
"I2KUYdI1ITqy7MRWq2w6MzmDVnJ4FeJOjb6yGuVcbaKkDYkal7QqRirF/KlMEDxATEwviGgauEUa1S7I",
"F0N6i0jUrJSXcM/xXD3D0wwQfyGPJ9nkIKgNsoLCO/0OlwVgXsMMeWFGjCS5LilmV0RWIRVnnnH7hgQW",
"xWg6rXNEX4kGHvBi+LhaoUX3x492UmJdGdhl3xtOuAeULBh1wLAnqn8BCgnNeAoRbwJ5qX9bKmYu8Hfc",
"oJNkoO1qmyrppaJ92zfjXOpHqoefuuqROyASe/6Hw39vZ1b1MLEsqgKfAgjDyotTSg421M5sqgHdQjRL",
"2UBca98qqeMklv8Qjd+QS+dHkMsblnJyU19oCnbybqdMwJwUN2QCKjnTD+EExUjdW7YROXnPttLnNJ+z",
"k0M/mBzS9vaFh1ONMjvhtIPCSd+g1eVUORriDgIMcRYN0TPGR0D8oOTFAkf+ke8/3zz//wAAAP//ndNk",
"W3R/AQA=",
"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",
"33+6fvrfAAAA//+BhLEjmoYBAA==",
}
// GetSwagger returns the content of the embedded swagger specification file
+32 -1
View File
@@ -58,6 +58,7 @@ import {
TenantInviteList,
TenantMember,
TenantMemberList,
TenantQueueMetrics,
TenantResourcePolicy,
TriggerWorkflowRunRequest,
UpdateTenantAlertEmailGroupRequest,
@@ -824,6 +825,36 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
secure: true,
...params,
});
/**
* @description Get the queue metrics for the tenant
*
* @tags Workflow
* @name TenantGetQueueMetrics
* @summary Get workflow metrics
* @request GET:/api/v1/tenants/{tenant}/queue-metrics
* @secure
*/
tenantGetQueueMetrics = (
tenant: string,
query?: {
/** A list of workflow IDs to filter by */
workflows?: WorkflowID[];
/**
* A list of metadata key value pairs to filter by
* @example ["key1:value1","key2:value2"]
*/
additionalMetadata?: string[];
},
params: RequestParams = {},
) =>
this.request<TenantQueueMetrics, APIErrors>({
path: `/api/v1/tenants/${tenant}/queue-metrics`,
method: "GET",
query: query,
secure: true,
format: "json",
...params,
});
/**
* @description Lists all events for a tenant.
*
@@ -1159,7 +1190,7 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
workflowGetMetrics = (
workflow: string,
query?: {
/** A status of workflow runs to filter by */
/** A status of workflow run statuses to filter by */
status?: WorkflowRunStatus;
/** A group key to filter metrics by */
groupKey?: string;
@@ -325,6 +325,21 @@ export interface TenantInviteList {
rows?: TenantInvite[];
}
export interface QueueMetrics {
/** The number of items in the queue. */
numQueued: number;
/** The number of items running. */
numRunning: number;
/** The number of items pending. */
numPending: number;
}
export interface TenantQueueMetrics {
/** The total queue metrics. */
total?: QueueMetrics;
workflow?: Record<string, QueueMetrics>;
}
export interface AcceptInviteRequest {
/**
* @minLength 36
+199 -1
View File
@@ -507,6 +507,18 @@ type PullRequest struct {
// PullRequestState defines model for PullRequestState.
type PullRequestState string
// QueueMetrics defines model for QueueMetrics.
type QueueMetrics struct {
// NumPending The number of items pending.
NumPending int `json:"numPending"`
// NumQueued The number of items in the queue.
NumQueued int `json:"numQueued"`
// NumRunning The number of items running.
NumRunning int `json:"numRunning"`
}
// RejectInviteRequest defines model for RejectInviteRequest.
type RejectInviteRequest struct {
Invite string `json:"invite" validate:"required,uuid"`
@@ -726,6 +738,12 @@ type TenantMemberList struct {
// TenantMemberRole defines model for TenantMemberRole.
type TenantMemberRole string
// TenantQueueMetrics defines model for TenantQueueMetrics.
type TenantQueueMetrics struct {
Total *QueueMetrics `json:"total,omitempty"`
Workflow *map[string]QueueMetrics `json:"workflow,omitempty"`
}
// TenantResource defines model for TenantResource.
type TenantResource string
@@ -1153,6 +1171,15 @@ type EventListParams struct {
AdditionalMetadata *[]string `form:"additionalMetadata,omitempty" json:"additionalMetadata,omitempty"`
}
// TenantGetQueueMetricsParams defines parameters for TenantGetQueueMetrics.
type TenantGetQueueMetricsParams struct {
// Workflows A list of workflow IDs to filter by
Workflows *[]WorkflowID `form:"workflows,omitempty" json:"workflows,omitempty"`
// AdditionalMetadata A list of metadata key value pairs to filter by
AdditionalMetadata *[]string `form:"additionalMetadata,omitempty" json:"additionalMetadata,omitempty"`
}
// WorkflowRunListPullRequestsParams defines parameters for WorkflowRunListPullRequests.
type WorkflowRunListPullRequestsParams struct {
// State The pull request state
@@ -1206,7 +1233,7 @@ type WorkflowRunGetMetricsParams struct {
// WorkflowGetMetricsParams defines parameters for WorkflowGetMetrics.
type WorkflowGetMetricsParams struct {
// Status A status of workflow runs to filter by
// Status A status of workflow run statuses to filter by
Status *WorkflowRunStatus `form:"status,omitempty" json:"status,omitempty"`
// GroupKey A group key to filter metrics by
@@ -1487,6 +1514,9 @@ type ClientInterface interface {
// TenantMemberDelete request
TenantMemberDelete(ctx context.Context, tenant openapi_types.UUID, member openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error)
// TenantGetQueueMetrics request
TenantGetQueueMetrics(ctx context.Context, tenant openapi_types.UUID, params *TenantGetQueueMetricsParams, reqEditors ...RequestEditorFn) (*http.Response, error)
// TenantResourcePolicyGet request
TenantResourcePolicyGet(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -2168,6 +2198,18 @@ func (c *Client) TenantMemberDelete(ctx context.Context, tenant openapi_types.UU
return c.Client.Do(req)
}
func (c *Client) TenantGetQueueMetrics(ctx context.Context, tenant openapi_types.UUID, params *TenantGetQueueMetricsParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewTenantGetQueueMetricsRequest(c.Server, tenant, params)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) TenantResourcePolicyGet(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewTenantResourcePolicyGetRequest(c.Server, tenant)
if err != nil {
@@ -4392,6 +4434,78 @@ func NewTenantMemberDeleteRequest(server string, tenant openapi_types.UUID, memb
return req, nil
}
// NewTenantGetQueueMetricsRequest generates requests for TenantGetQueueMetrics
func NewTenantGetQueueMetricsRequest(server string, tenant openapi_types.UUID, params *TenantGetQueueMetricsParams) (*http.Request, error) {
var err error
var pathParam0 string
pathParam0, err = runtime.StyleParamWithLocation("simple", false, "tenant", runtime.ParamLocationPath, tenant)
if err != nil {
return nil, err
}
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/api/v1/tenants/%s/queue-metrics", pathParam0)
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
if params != nil {
queryValues := queryURL.Query()
if params.Workflows != nil {
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "workflows", runtime.ParamLocationQuery, *params.Workflows); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
} else {
for k, v := range parsed {
for _, v2 := range v {
queryValues.Add(k, v2)
}
}
}
}
if params.AdditionalMetadata != nil {
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "additionalMetadata", runtime.ParamLocationQuery, *params.AdditionalMetadata); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
} else {
for k, v := range parsed {
for _, v2 := range v {
queryValues.Add(k, v2)
}
}
}
}
queryURL.RawQuery = queryValues.Encode()
}
req, err := http.NewRequest("GET", queryURL.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// NewTenantResourcePolicyGetRequest generates requests for TenantResourcePolicyGet
func NewTenantResourcePolicyGetRequest(server string, tenant openapi_types.UUID) (*http.Request, error) {
var err error
@@ -6327,6 +6441,9 @@ type ClientWithResponsesInterface interface {
// TenantMemberDeleteWithResponse request
TenantMemberDeleteWithResponse(ctx context.Context, tenant openapi_types.UUID, member openapi_types.UUID, reqEditors ...RequestEditorFn) (*TenantMemberDeleteResponse, error)
// TenantGetQueueMetricsWithResponse request
TenantGetQueueMetricsWithResponse(ctx context.Context, tenant openapi_types.UUID, params *TenantGetQueueMetricsParams, reqEditors ...RequestEditorFn) (*TenantGetQueueMetricsResponse, error)
// TenantResourcePolicyGetWithResponse request
TenantResourcePolicyGetWithResponse(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*TenantResourcePolicyGetResponse, error)
@@ -7328,6 +7445,31 @@ func (r TenantMemberDeleteResponse) StatusCode() int {
return 0
}
type TenantGetQueueMetricsResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *TenantQueueMetrics
JSON400 *APIErrors
JSON403 *APIErrors
JSON404 *APIErrors
}
// Status returns HTTPResponse.Status
func (r TenantGetQueueMetricsResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r TenantGetQueueMetricsResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type TenantResourcePolicyGetResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -8676,6 +8818,15 @@ func (c *ClientWithResponses) TenantMemberDeleteWithResponse(ctx context.Context
return ParseTenantMemberDeleteResponse(rsp)
}
// TenantGetQueueMetricsWithResponse request returning *TenantGetQueueMetricsResponse
func (c *ClientWithResponses) TenantGetQueueMetricsWithResponse(ctx context.Context, tenant openapi_types.UUID, params *TenantGetQueueMetricsParams, reqEditors ...RequestEditorFn) (*TenantGetQueueMetricsResponse, error) {
rsp, err := c.TenantGetQueueMetrics(ctx, tenant, params, reqEditors...)
if err != nil {
return nil, err
}
return ParseTenantGetQueueMetricsResponse(rsp)
}
// TenantResourcePolicyGetWithResponse request returning *TenantResourcePolicyGetResponse
func (c *ClientWithResponses) TenantResourcePolicyGetWithResponse(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*TenantResourcePolicyGetResponse, error) {
rsp, err := c.TenantResourcePolicyGet(ctx, tenant, reqEditors...)
@@ -10522,6 +10673,53 @@ func ParseTenantMemberDeleteResponse(rsp *http.Response) (*TenantMemberDeleteRes
return response, nil
}
// ParseTenantGetQueueMetricsResponse parses an HTTP response from a TenantGetQueueMetricsWithResponse call
func ParseTenantGetQueueMetricsResponse(rsp *http.Response) (*TenantGetQueueMetricsResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &TenantGetQueueMetricsResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest TenantQueueMetrics
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
var dest APIErrors
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON400 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
var dest APIErrors
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON403 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
var dest APIErrors
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON404 = &dest
}
return response, nil
}
// ParseTenantResourcePolicyGetResponse parses an HTTP response from a TenantResourcePolicyGetWithResponse call
func ParseTenantResourcePolicyGetResponse(rsp *http.Response) (*TenantResourcePolicyGetResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
+77
View File
@@ -51,3 +51,80 @@ SET
WHERE
"tenantId" = sqlc.arg('tenantId')::uuid
RETURNING *;
-- name: GetTenantTotalQueueMetrics :one
WITH valid_workflow_runs AS (
SELECT
runs."id", workflow."id" as "workflowId", workflow."name" as "workflowName"
FROM
"WorkflowRun" as runs
LEFT JOIN
"WorkflowVersion" as workflowVersion ON runs."workflowVersionId" = workflowVersion."id"
LEFT JOIN
"Workflow" as workflow ON workflowVersion."workflowId" = workflow."id"
WHERE
-- status of the workflow run must be pending, queued or running
runs."status" IN ('PENDING', 'QUEUED', 'RUNNING') AND
runs."tenantId" = $1 AND
(
sqlc.narg('additionalMetadata')::jsonb IS NULL OR
runs."additionalMetadata" @> sqlc.narg('additionalMetadata')::jsonb
) AND
(
sqlc.narg('workflowIds')::uuid[] IS NULL OR
workflow."id" = ANY(sqlc.narg('workflowIds')::uuid[])
)
)
SELECT
-- count of step runs in a PENDING_ASSIGNMENT state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'PENDING_ASSIGNMENT') as "pendingAssignmentCount",
-- count of step runs in a PENDING state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'PENDING') as "pendingCount",
-- count of step runs in a RUNNING state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'RUNNING') as "runningCount"
FROM
valid_workflow_runs as runs
LEFT JOIN
"JobRun" as jobRun ON runs."id" = jobRun."workflowRunId"
LEFT JOIN
"StepRun" as stepRun ON jobRun."id" = stepRun."jobRunId";
-- name: GetTenantWorkflowQueueMetrics :many
WITH valid_workflow_runs AS (
SELECT
runs."id", workflow."id" as "workflowId", workflow."name" as "workflowName"
FROM
"WorkflowRun" as runs
LEFT JOIN
"WorkflowVersion" as workflowVersion ON runs."workflowVersionId" = workflowVersion."id"
LEFT JOIN
"Workflow" as workflow ON workflowVersion."workflowId" = workflow."id"
WHERE
-- status of the workflow run must be pending, queued or running
runs."status" IN ('PENDING', 'QUEUED', 'RUNNING') AND
runs."tenantId" = $1 AND
(
sqlc.narg('additionalMetadata')::jsonb IS NULL OR
runs."additionalMetadata" @> sqlc.narg('additionalMetadata')::jsonb
) AND
(
sqlc.narg('workflowIds')::uuid[] IS NULL OR
workflow."id" = ANY(sqlc.narg('workflowIds')::uuid[])
)
)
SELECT
runs."workflowId",
-- count of step runs in a PENDING_ASSIGNMENT state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'PENDING_ASSIGNMENT') as "pendingAssignmentCount",
-- count of step runs in a PENDING state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'PENDING') as "pendingCount",
-- count of step runs in a RUNNING state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'RUNNING') as "runningCount"
FROM
valid_workflow_runs as runs
LEFT JOIN
"JobRun" as jobRun ON runs."id" = jobRun."workflowRunId"
LEFT JOIN
"StepRun" as stepRun ON jobRun."id" = stepRun."jobRunId"
GROUP BY
runs."workflowId";
+136
View File
@@ -168,6 +168,142 @@ func (q *Queries) GetTenantByID(ctx context.Context, db DBTX, id pgtype.UUID) (*
return &i, err
}
const getTenantTotalQueueMetrics = `-- name: GetTenantTotalQueueMetrics :one
WITH valid_workflow_runs AS (
SELECT
runs."id", workflow."id" as "workflowId", workflow."name" as "workflowName"
FROM
"WorkflowRun" as runs
LEFT JOIN
"WorkflowVersion" as workflowVersion ON runs."workflowVersionId" = workflowVersion."id"
LEFT JOIN
"Workflow" as workflow ON workflowVersion."workflowId" = workflow."id"
WHERE
-- status of the workflow run must be pending, queued or running
runs."status" IN ('PENDING', 'QUEUED', 'RUNNING') AND
runs."tenantId" = $1 AND
(
$2::jsonb IS NULL OR
runs."additionalMetadata" @> $2::jsonb
) AND
(
$3::uuid[] IS NULL OR
workflow."id" = ANY($3::uuid[])
)
)
SELECT
-- count of step runs in a PENDING_ASSIGNMENT state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'PENDING_ASSIGNMENT') as "pendingAssignmentCount",
-- count of step runs in a PENDING state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'PENDING') as "pendingCount",
-- count of step runs in a RUNNING state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'RUNNING') as "runningCount"
FROM
valid_workflow_runs as runs
LEFT JOIN
"JobRun" as jobRun ON runs."id" = jobRun."workflowRunId"
LEFT JOIN
"StepRun" as stepRun ON jobRun."id" = stepRun."jobRunId"
`
type GetTenantTotalQueueMetricsParams struct {
TenantId pgtype.UUID `json:"tenantId"`
AdditionalMetadata []byte `json:"additionalMetadata"`
WorkflowIds []pgtype.UUID `json:"workflowIds"`
}
type GetTenantTotalQueueMetricsRow struct {
PendingAssignmentCount int64 `json:"pendingAssignmentCount"`
PendingCount int64 `json:"pendingCount"`
RunningCount int64 `json:"runningCount"`
}
func (q *Queries) GetTenantTotalQueueMetrics(ctx context.Context, db DBTX, arg GetTenantTotalQueueMetricsParams) (*GetTenantTotalQueueMetricsRow, error) {
row := db.QueryRow(ctx, getTenantTotalQueueMetrics, arg.TenantId, arg.AdditionalMetadata, arg.WorkflowIds)
var i GetTenantTotalQueueMetricsRow
err := row.Scan(&i.PendingAssignmentCount, &i.PendingCount, &i.RunningCount)
return &i, err
}
const getTenantWorkflowQueueMetrics = `-- name: GetTenantWorkflowQueueMetrics :many
WITH valid_workflow_runs AS (
SELECT
runs."id", workflow."id" as "workflowId", workflow."name" as "workflowName"
FROM
"WorkflowRun" as runs
LEFT JOIN
"WorkflowVersion" as workflowVersion ON runs."workflowVersionId" = workflowVersion."id"
LEFT JOIN
"Workflow" as workflow ON workflowVersion."workflowId" = workflow."id"
WHERE
-- status of the workflow run must be pending, queued or running
runs."status" IN ('PENDING', 'QUEUED', 'RUNNING') AND
runs."tenantId" = $1 AND
(
$2::jsonb IS NULL OR
runs."additionalMetadata" @> $2::jsonb
) AND
(
$3::uuid[] IS NULL OR
workflow."id" = ANY($3::uuid[])
)
)
SELECT
runs."workflowId",
-- count of step runs in a PENDING_ASSIGNMENT state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'PENDING_ASSIGNMENT') as "pendingAssignmentCount",
-- count of step runs in a PENDING state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'PENDING') as "pendingCount",
-- count of step runs in a RUNNING state
COUNT(stepRun."id") FILTER (WHERE stepRun."status" = 'RUNNING') as "runningCount"
FROM
valid_workflow_runs as runs
LEFT JOIN
"JobRun" as jobRun ON runs."id" = jobRun."workflowRunId"
LEFT JOIN
"StepRun" as stepRun ON jobRun."id" = stepRun."jobRunId"
GROUP BY
runs."workflowId"
`
type GetTenantWorkflowQueueMetricsParams struct {
TenantId pgtype.UUID `json:"tenantId"`
AdditionalMetadata []byte `json:"additionalMetadata"`
WorkflowIds []pgtype.UUID `json:"workflowIds"`
}
type GetTenantWorkflowQueueMetricsRow struct {
WorkflowId pgtype.UUID `json:"workflowId"`
PendingAssignmentCount int64 `json:"pendingAssignmentCount"`
PendingCount int64 `json:"pendingCount"`
RunningCount int64 `json:"runningCount"`
}
func (q *Queries) GetTenantWorkflowQueueMetrics(ctx context.Context, db DBTX, arg GetTenantWorkflowQueueMetricsParams) ([]*GetTenantWorkflowQueueMetricsRow, error) {
rows, err := db.Query(ctx, getTenantWorkflowQueueMetrics, arg.TenantId, arg.AdditionalMetadata, arg.WorkflowIds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []*GetTenantWorkflowQueueMetricsRow
for rows.Next() {
var i GetTenantWorkflowQueueMetricsRow
if err := rows.Scan(
&i.WorkflowId,
&i.PendingAssignmentCount,
&i.PendingCount,
&i.RunningCount,
); err != nil {
return nil, err
}
items = append(items, &i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listTenants = `-- name: ListTenants :many
SELECT
id, "createdAt", "updatedAt", "deletedAt", name, slug, "analyticsOptOut", "alertMemberEmails"
+1 -1
View File
@@ -92,7 +92,7 @@ func NewAPIRepository(client *db.PrismaClient, pool *pgxpool.Pool, fs ...PrismaR
apiToken: NewAPITokenRepository(client, opts.v, opts.cache),
event: NewEventAPIRepository(client, pool, opts.v, opts.l),
log: NewLogAPIRepository(pool, opts.v, opts.l),
tenant: NewTenantAPIRepository(client, opts.v, opts.cache),
tenant: NewTenantAPIRepository(pool, client, opts.v, opts.l, opts.cache),
tenantAlerting: NewTenantAlertingAPIRepository(client, opts.v, opts.cache),
tenantInvite: NewTenantInviteRepository(client, opts.v),
workflow: NewWorkflowRepository(client, pool, opts.v, opts.l),
+93 -7
View File
@@ -2,8 +2,10 @@ package prisma
import (
"context"
"encoding/json"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog"
@@ -16,16 +18,24 @@ import (
)
type tenantAPIRepository struct {
client *db.PrismaClient
v validator.Validator
cache cache.Cacheable
cache cache.Cacheable
client *db.PrismaClient
pool *pgxpool.Pool
v validator.Validator
l *zerolog.Logger
queries *dbsqlc.Queries
}
func NewTenantAPIRepository(client *db.PrismaClient, v validator.Validator, cache cache.Cacheable) repository.TenantAPIRepository {
func NewTenantAPIRepository(pool *pgxpool.Pool, client *db.PrismaClient, v validator.Validator, l *zerolog.Logger, cache cache.Cacheable) repository.TenantAPIRepository {
queries := dbsqlc.New()
return &tenantAPIRepository{
client: client,
v: v,
cache: cache,
cache: cache,
client: client,
pool: pool,
v: v,
l: l,
queries: queries,
}
}
@@ -169,6 +179,82 @@ func (r *tenantAPIRepository) DeleteTenantMember(memberId string) (*db.TenantMem
).Delete().Exec(context.Background())
}
func (r *tenantAPIRepository) GetQueueMetrics(tenantId string, opts *repository.GetQueueMetricsOpts) (*repository.GetQueueMetricsResponse, error) {
if err := r.v.Validate(opts); err != nil {
return nil, err
}
totalParams := dbsqlc.GetTenantTotalQueueMetricsParams{
TenantId: sqlchelpers.UUIDFromStr(tenantId),
}
workflowParams := dbsqlc.GetTenantWorkflowQueueMetricsParams{
TenantId: sqlchelpers.UUIDFromStr(tenantId),
}
if opts.AdditionalMetadata != nil {
additionalMetadataBytes, err := json.Marshal(opts.AdditionalMetadata)
if err != nil {
return nil, err
}
totalParams.AdditionalMetadata = additionalMetadataBytes
workflowParams.AdditionalMetadata = additionalMetadataBytes
}
if opts.WorkflowIds != nil {
uuids := make([]pgtype.UUID, len(opts.WorkflowIds))
for i, id := range opts.WorkflowIds {
uuids[i] = sqlchelpers.UUIDFromStr(id)
}
workflowParams.WorkflowIds = uuids
totalParams.WorkflowIds = uuids
}
tx, err := r.pool.Begin(context.Background())
if err != nil {
return nil, err
}
defer deferRollback(context.Background(), r.l, tx.Rollback)
// get the totals
total, err := r.queries.GetTenantTotalQueueMetrics(context.Background(), tx, totalParams)
if err != nil {
return nil, err
}
// get the workflow metrics
workflowMetrics, err := r.queries.GetTenantWorkflowQueueMetrics(context.Background(), tx, workflowParams)
if err != nil {
return nil, err
}
workflowMetricsMap := make(map[string]repository.QueueMetric)
for _, metric := range workflowMetrics {
workflowMetricsMap[sqlchelpers.UUIDToStr(metric.WorkflowId)] = repository.QueueMetric{
PendingAssignment: int(metric.PendingAssignmentCount),
Pending: int(metric.PendingCount),
Running: int(metric.RunningCount),
}
}
return &repository.GetQueueMetricsResponse{
Total: repository.QueueMetric{
PendingAssignment: int(total.PendingAssignmentCount),
Pending: int(total.PendingCount),
Running: int(total.RunningCount),
},
ByWorkflowId: workflowMetricsMap,
}, nil
}
type tenantEngineRepository struct {
cache cache.Cacheable
pool *pgxpool.Pool
+28
View File
@@ -35,6 +35,31 @@ type UpdateTenantMemberOpts struct {
Role *string `validate:"omitempty,oneof=OWNER ADMIN MEMBER"`
}
type GetQueueMetricsOpts struct {
// (optional) a list of workflow ids to filter by
WorkflowIds []string `validate:"omitempty,dive,uuid"`
// (optional) exact metadata to filter by
AdditionalMetadata map[string]interface{} `validate:"omitempty"`
}
type QueueMetric struct {
// the total number of PENDING_ASSIGNMENT step runs in the queue
PendingAssignment int `json:"pending_assignment"`
// the total number of PENDING step runs in the queue
Pending int `json:"pending"`
// the total number of RUNNING step runs in the queue
Running int `json:"running"`
}
type GetQueueMetricsResponse struct {
Total QueueMetric `json:"total"`
ByWorkflowId map[string]QueueMetric `json:"by_workflow"`
}
type TenantAPIRepository interface {
// CreateTenant creates a new tenant.
CreateTenant(opts *CreateTenantOpts) (*db.TenantModel, error)
@@ -68,6 +93,9 @@ type TenantAPIRepository interface {
// DeleteTenantMember deletes the tenant member with the given id
DeleteTenantMember(memberId string) (*db.TenantMemberModel, error)
// GetQueueMetrics returns the queue metrics for the given tenant
GetQueueMetrics(tenantId string, opts *GetQueueMetricsOpts) (*GetQueueMetricsResponse, error)
}
type TenantEngineRepository interface {