From 77605055b0eac3899208e340260742bb4f980bec Mon Sep 17 00:00:00 2001 From: Sven Efftinge Date: Tue, 6 Sep 2022 07:24:20 +0000 Subject: [PATCH] Added API call to fetch usage data --- .../gitpod-protocol/src/gitpod-service.ts | 3 +- components/gitpod-protocol/src/usage.ts | 65 +++++++++++++++++++ .../ee/src/workspace/gitpod-server-impl.ts | 65 ++++++++++++++++++- components/server/src/auth/rate-limiter.ts | 1 + .../src/workspace/gitpod-server-impl.ts | 11 +++- .../typescript/src/usage/v1/sugar.ts | 30 ++++++++- components/usage/pkg/db/usage.go | 13 ++++ 7 files changed, 182 insertions(+), 6 deletions(-) diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 5d78db0c5fedae..4f736413ca0fe9 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -60,7 +60,7 @@ import { import { RemotePageMessage, RemoteTrackMessage, RemoteIdentifyMessage } from "./analytics"; import { IDEServer } from "./ide-protocol"; import { InstallationAdminSettings, TelemetryData } from "./installation-admin-protocol"; -import { ListBilledUsageResponse, ListBilledUsageRequest } from "./usage"; +import { ListBilledUsageResponse, ListBilledUsageRequest, ListUsageRequest, ListUsageResponse } from "./usage"; import { SupportedWorkspaceClass } from "./workspace-class"; import { BillingMode } from "./billing-mode"; @@ -298,6 +298,7 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, setSpendingLimitForTeam(teamId: string, spendingLimit: number): Promise; listBilledUsage(req: ListBilledUsageRequest): Promise; + listUsage(req: ListUsageRequest): Promise; setUsageAttribution(usageAttribution: string): Promise; diff --git a/components/gitpod-protocol/src/usage.ts b/components/gitpod-protocol/src/usage.ts index 079b5aea0e913b..1eef92079ef7f3 100644 --- a/components/gitpod-protocol/src/usage.ts +++ b/components/gitpod-protocol/src/usage.ts @@ -61,3 +61,68 @@ export interface ListBilledUsageResponse { } export type BillableWorkspaceType = WorkspaceType; + +// types below are copied over from components/usage-api/typescript/src/usage/v1/usage_pb.d.ts + +export interface ListUsageRequest { + attributionId: string; + from?: number; + to?: number; + order: Ordering; + pagination?: PaginationRequest; +} + +export enum Ordering { + ORDERING_DESCENDING = 0, + ORDERING_ASCENDING = 1, +} + +export interface PaginationRequest { + perPage: number; + page: number; +} + +export interface ListUsageResponse { + usageEntriesList: Usage[]; + pagination?: PaginationResponse; + creditBalanceAtStart: number; + creditBalanceAtEnd: number; +} + +export interface PaginationResponse { + perPage: number; + totalPages: number; + total: number; + page: number; +} + +export type UsageKind = "workspaceinstance" | "invoice"; +export interface Usage { + id: string; + attributionId: string; + description: string; + credits: number; + effectiveTime?: number; + kind: UsageKind; + workspaceInstanceId: string; + draft: boolean; + metadata: WorkspaceInstanceUsageData | InvoiceUsageData; +} + +// the equivalent golang shape is maintained in `/workspace/gitpod/`components/usage/pkg/db/usage.go` +export interface WorkspaceInstanceUsageData { + workspaceId: string; + workspaceType: WorkspaceType; + workspaceClass: string; + contextURL: string; + startTime: string; + endTime?: string; + userName: string; + userAvatarURL: string; +} + +export interface InvoiceUsageData { + invoiceId: string; + startDate: string; + endDate: string; +} diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 811e496a66ddcc..e84cfd6a72f640 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -71,8 +71,13 @@ import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositor import { EligibilityService } from "../user/eligibility-service"; import { AccountStatementProvider } from "../user/account-statement-provider"; import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol"; -import { ListBilledUsageRequest, ListBilledUsageResponse } from "@gitpod/gitpod-protocol/lib/usage"; -import { ListBilledUsageRequest as ListBilledUsage } from "@gitpod/usage-api/lib/usage/v1/usage_pb"; +import { + ListBilledUsageRequest, + ListBilledUsageResponse, + ListUsageRequest, + ListUsageResponse, +} from "@gitpod/gitpod-protocol/lib/usage"; +import * as usage_grpc from "@gitpod/usage-api/lib/usage/v1/usage_pb"; import { AssigneeIdentityIdentifier, TeamSubscription, @@ -2182,6 +2187,60 @@ export class GitpodServerEEImpl extends GitpodServerImpl { return result; } + async listUsage(ctx: TraceContext, req: ListUsageRequest): Promise { + const { attributionId, from, to } = req; + traceAPIParams(ctx, { attributionId }); + const user = this.checkAndBlockUser("listBilledUsage"); + + await this.guardCostCenterAccess(ctx, user.id, attributionId, "get"); + + const timestampFrom = from ? Timestamp.fromDate(new Date(from)) : undefined; + const timestampTo = to ? Timestamp.fromDate(new Date(to)) : undefined; + + const usageClient = this.usageServiceClientProvider.getDefault(); + const request = new usage_grpc.ListBilledUsageRequest(); + request.setAttributionId(attributionId); + request.setFrom(timestampFrom); + if (to) { + request.setTo(timestampTo); + } + request.setOrder(req.order); + if (req.pagination) { + const paginatedRequest = new usage_grpc.PaginatedRequest(); + paginatedRequest.setPage(req.pagination.page); + paginatedRequest.setPerPage(req.pagination.perPage); + request.setPagination(paginatedRequest); + } + const response = await usageClient.listUsage(ctx, request); + const pagination = response.getPagination(); + return { + usageEntriesList: response.getUsageEntriesList().map((u) => { + return { + id: u.getId(), + attributionId: u.getAttributionId(), + effectiveTime: u.getEffectiveTime()!.toDate().getTime(), + credits: u.getCredits(), + description: u.getDescription(), + draft: u.getDraft(), + workspaceInstanceId: u.getWorkspaceInstanceId(), + kind: + u.getKind() === usage_grpc.Usage.Kind.KIND_WORKSPACE_INSTANCE ? "workspaceinstance" : "invoice", + metadata: JSON.parse(u.getMetadata()), + }; + }), + pagination: pagination + ? { + page: pagination.getPage(), + perPage: pagination.getPerPage(), + total: pagination.getTotal(), + totalPages: pagination.getTotalPages(), + } + : undefined, + creditBalanceAtEnd: response.getCreditBalanceAtEnd(), + creditBalanceAtStart: response.getCreditBalanceAtStart(), + }; + } + async listBilledUsage(ctx: TraceContext, req: ListBilledUsageRequest): Promise { const { attributionId, fromDate, toDate, perPage, page } = req; traceAPIParams(ctx, { attributionId }); @@ -2201,7 +2260,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const response = await usageClient.listBilledUsage( ctx, attributionId, - ListBilledUsage.Ordering.ORDERING_DESCENDING, + usage_grpc.ListBilledUsageRequest.Ordering.ORDERING_DESCENDING, perPage, page, timestampFrom, diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index bf704ba7ad7651..94893388ac4252 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -210,6 +210,7 @@ const defaultFunctions: FunctionsConfig = { subscribeTeamToStripe: { group: "default", points: 1 }, getStripePortalUrlForTeam: { group: "default", points: 1 }, listBilledUsage: { group: "default", points: 1 }, + listUsage: { group: "default", points: 1 }, getBillingModeForTeam: { group: "default", points: 1 }, getBillingModeForUser: { group: "default", points: 1 }, diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 5fd2fa71d63c96..e6c408a4ee742c 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -172,7 +172,12 @@ import { InstallationAdminTelemetryDataProvider } from "../installation-admin/te import { LicenseEvaluator } from "@gitpod/licensor/lib"; import { Feature } from "@gitpod/licensor/lib/api"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; -import { ListBilledUsageRequest, ListBilledUsageResponse } from "@gitpod/gitpod-protocol/lib/usage"; +import { + ListBilledUsageRequest, + ListBilledUsageResponse, + ListUsageRequest, + ListUsageResponse, +} from "@gitpod/gitpod-protocol/lib/usage"; import { WorkspaceClusterImagebuilderClientProvider } from "./workspace-cluster-imagebuilder-client-provider"; import { VerificationService } from "../auth/verification-service"; import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; @@ -3232,6 +3237,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } + async listUsage(ctx: TraceContext, req: ListUsageRequest): Promise { + throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); + } + async getSpendingLimitForTeam(ctx: TraceContext, teamId: string): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } diff --git a/components/usage-api/typescript/src/usage/v1/sugar.ts b/components/usage-api/typescript/src/usage/v1/sugar.ts index 4baa9216bd6b20..68d4e877260e2d 100644 --- a/components/usage-api/typescript/src/usage/v1/sugar.ts +++ b/components/usage-api/typescript/src/usage/v1/sugar.ts @@ -9,7 +9,7 @@ import { BillingServiceClient } from "./billing_grpc_pb"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import * as opentracing from "opentracing"; import { Metadata } from "@grpc/grpc-js"; -import { BilledSession, ListBilledUsageRequest, ListBilledUsageResponse, PaginatedRequest } from "./usage_pb"; +import { BilledSession, ListBilledUsageRequest, ListBilledUsageResponse, ListUsageRequest, ListUsageResponse, PaginatedRequest } from "./usage_pb"; import { GetUpcomingInvoiceRequest, GetUpcomingInvoiceResponse, @@ -194,6 +194,34 @@ export class PromisifiedUsageServiceClient { } } + public async listUsage( + _ctx: TraceContext, + request: ListUsageRequest, + ): Promise { + const ctx = TraceContext.childContext(`/usage-service/listUsage`, _ctx); + try { + const response = await new Promise((resolve, reject) => { + this.client.listUsage( + request, + withTracing(ctx), + (err: grpc.ServiceError | null, response: ListUsageResponse) => { + if (err) { + reject(err); + return; + } + resolve(response); + }, + ); + }); + return response; + } catch (err) { + TraceContext.setError(ctx, err); + throw err; + } finally { + ctx.span.finish(); + } + } + public dispose() { this.client.close(); } diff --git a/components/usage/pkg/db/usage.go b/components/usage/pkg/db/usage.go index dd3b95b268cbdc..f16f5d8dbcfc5c 100644 --- a/components/usage/pkg/db/usage.go +++ b/components/usage/pkg/db/usage.go @@ -47,6 +47,19 @@ type Usage struct { Metadata datatypes.JSON `gorm:"column:metadata;type:text;size:65535" json:"metadata"` } +// WorkspaceInstanceUsageData represents the shape of metadata for usage entries of kind "workspaceinstance" +// the equivalent TypeScript definition is maintained in `components/gitpod-protocol/src/usage.ts“ +type WorkspaceInstanceUsageData struct { + WorkspaceId string `json:"workspaceId"` + WorkspaceType WorkspaceType `json:"workspaceType"` + WorkspaceClass string `json:"workspaceClass"` + ContextURL string `json:"contextURL"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + UserName string `json:"userName"` + UserAvatarURL string `json:"userAvatarURL"` +} + type FindUsageResult struct { UsageEntries []Usage }