Skip to content

Commit 7e21e49

Browse files
committed
[server] Hook up usage API
1 parent 969cb86 commit 7e21e49

File tree

11 files changed

+90
-25
lines changed

11 files changed

+90
-25
lines changed

components/dashboard/src/teams/TeamUsage.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getTeamSettingsMenu } from "./TeamSettings";
1212
import { PaymentContext } from "../payment-context";
1313
import { getGitpodService } from "../service/service";
1414
import { BillableSession, BillableWorkspaceType } from "@gitpod/gitpod-protocol/lib/usage";
15+
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
1516
import { Item, ItemField, ItemsList } from "../components/ItemsList";
1617
import moment from "moment";
1718
import Property from "../admin/Property";
@@ -29,7 +30,8 @@ function TeamUsage() {
2930
return;
3031
}
3132
(async () => {
32-
const billedUsageResult = await getGitpodService().server.getBilledUsage("some-attribution-id");
33+
const attributionId = AttributionId.render({ kind: "team", teamId: team.id });
34+
const billedUsageResult = await getGitpodService().server.getBilledUsage(attributionId);
3335
setBilledUsage(billedUsageResult);
3436
})();
3537
}, [team]);
@@ -92,7 +94,9 @@ function TeamUsage() {
9294
<span className="text-gray-400">{usage.workspaceClass}</span>
9395
</div>
9496
<div className="my-auto">
95-
<span className="text-gray-700">{getHours(usage.endTime, usage.startTime)}</span>
97+
<span className="text-gray-700">
98+
{getHours(new Date(usage.endTime).getTime(), new Date(usage.startTime).getTime())}
99+
</span>
96100
</div>
97101
<div className="my-auto">
98102
<span className="text-gray-700">{usage.credits}</span>

components/gitpod-protocol/src/usage.ts

+19-19
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export interface BillableSession {
2424
workspaceClass: string;
2525

2626
// When the workspace started
27-
startTime: number;
27+
startTime: string;
2828

2929
// When the workspace ended
30-
endTime: number;
30+
endTime: string;
3131

3232
// The credits used for this session
3333
credits: number;
@@ -36,7 +36,7 @@ export interface BillableSession {
3636
projectId?: string;
3737
}
3838

39-
export type BillableWorkspaceType = Omit<WorkspaceType, "probe">;
39+
export type BillableWorkspaceType = WorkspaceType;
4040

4141
export const billableSessionDummyData: BillableSession[] = [
4242
{
@@ -47,8 +47,8 @@ export const billableSessionDummyData: BillableSession[] = [
4747
workspaceId: "some-workspace-id",
4848
workspaceType: "prebuild",
4949
workspaceClass: "XL",
50-
startTime: Date.now() + -3 * 24 * 3600 * 1000, // 3 days ago
51-
endTime: Date.now(),
50+
startTime: new Date(Date.now() + -3 * 24 * 3600 * 1000).toISOString(), // 3 days ago
51+
endTime: new Date().toISOString(),
5252
credits: 320,
5353
projectId: "project-123",
5454
},
@@ -60,8 +60,8 @@ export const billableSessionDummyData: BillableSession[] = [
6060
workspaceId: "some-workspace-id2",
6161
workspaceType: "regular",
6262
workspaceClass: "standard",
63-
startTime: Date.now() + -5 * 24 * 3600 * 1000,
64-
endTime: Date.now(),
63+
startTime: new Date(Date.now() + -5 * 24 * 3600 * 1000).toISOString(),
64+
endTime: new Date().toISOString(),
6565
credits: 130,
6666
projectId: "project-123",
6767
},
@@ -73,8 +73,8 @@ export const billableSessionDummyData: BillableSession[] = [
7373
workspaceId: "some-workspace-id3",
7474
workspaceType: "regular",
7575
workspaceClass: "XL",
76-
startTime: Date.now() + -5 * 24 * 3600 * 1000,
77-
endTime: Date.now() + -4 * 24 * 3600 * 1000,
76+
startTime: new Date(Date.now() + -5 * 24 * 3600 * 1000).toISOString(),
77+
endTime: new Date(Date.now() + -4 * 24 * 3600 * 1000).toISOString(),
7878
credits: 150,
7979
projectId: "project-134",
8080
},
@@ -86,8 +86,8 @@ export const billableSessionDummyData: BillableSession[] = [
8686
workspaceId: "some-workspace-id4",
8787
workspaceType: "regular",
8888
workspaceClass: "standard",
89-
startTime: Date.now() + -10 * 24 * 3600 * 1000,
90-
endTime: Date.now() + -9 * 24 * 3600 * 1000,
89+
startTime: new Date(Date.now() + -10 * 24 * 3600 * 1000).toISOString(),
90+
endTime: new Date(Date.now() + -9 * 24 * 3600 * 1000).toISOString(),
9191
credits: 330,
9292
projectId: "project-137",
9393
},
@@ -99,8 +99,8 @@ export const billableSessionDummyData: BillableSession[] = [
9999
workspaceId: "some-workspace-id5",
100100
workspaceType: "regular",
101101
workspaceClass: "XL",
102-
startTime: Date.now() + -2 * 24 * 3600 * 1000,
103-
endTime: Date.now(),
102+
startTime: new Date(Date.now() + -2 * 24 * 3600 * 1000).toISOString(),
103+
endTime: new Date().toISOString(),
104104
credits: 222,
105105
projectId: "project-138",
106106
},
@@ -112,8 +112,8 @@ export const billableSessionDummyData: BillableSession[] = [
112112
workspaceId: "some-workspace-id3",
113113
workspaceType: "regular",
114114
workspaceClass: "XL",
115-
startTime: Date.now() + -7 * 24 * 3600 * 1000,
116-
endTime: Date.now() + -6 * 24 * 3600 * 1000,
115+
startTime: new Date(Date.now() + -7 * 24 * 3600 * 1000).toISOString(),
116+
endTime: new Date(Date.now() + -6 * 24 * 3600 * 1000).toISOString(),
117117
credits: 300,
118118
projectId: "project-134",
119119
},
@@ -125,8 +125,8 @@ export const billableSessionDummyData: BillableSession[] = [
125125
workspaceId: "some-workspace-id3",
126126
workspaceType: "regular",
127127
workspaceClass: "standard",
128-
startTime: Date.now() + -1 * 24 * 3600 * 1000,
129-
endTime: Date.now(),
128+
startTime: new Date(Date.now() + -1 * 24 * 3600 * 1000).toISOString(),
129+
endTime: new Date().toISOString(),
130130
credits: 100,
131131
projectId: "project-567",
132132
},
@@ -138,8 +138,8 @@ export const billableSessionDummyData: BillableSession[] = [
138138
workspaceId: "some-workspace-id7",
139139
workspaceType: "prebuild",
140140
workspaceClass: "XL",
141-
startTime: Date.now() + -1 * 24 * 3600 * 1000,
142-
endTime: Date.now(),
141+
startTime: new Date(Date.now() + -1 * 24 * 3600 * 1000).toISOString(),
142+
endTime: new Date().toISOString(),
143143
credits: 200,
144144
projectId: "project-345",
145145
},

components/server/BUILD.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ packages:
1717
- components/licensor/typescript:lib
1818
- components/ws-manager-api/typescript:lib
1919
- components/supervisor-api/typescript-grpcweb:lib
20+
- components/usage-api/typescript:lib
2021
config:
2122
packaging: offline-mirror
2223
yarnLock: ${coreYarnLockBase}/yarn.lock
@@ -55,6 +56,7 @@ packages:
5556
- components/licensor/typescript:lib
5657
- components/ws-manager-api/typescript:lib
5758
- components/supervisor-api/typescript-grpcweb:lib
59+
- components/usage-api/typescript:lib
5860
- :dbtest
5961
config:
6062
packaging: library
@@ -80,6 +82,7 @@ packages:
8082
- components/licensor/typescript:lib
8183
- components/ws-manager-api/typescript:lib
8284
- components/supervisor-api/typescript-grpcweb:lib
85+
- components/usage-api/typescript:lib
8386
config:
8487
packaging: library
8588
yarnLock: ${coreYarnLockBase}/yarn.lock

components/server/ee/src/workspace/gitpod-server-impl.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
FindPrebuildsParams,
4747
TeamMemberRole,
4848
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
49+
WorkspaceType,
4950
} from "@gitpod/gitpod-protocol";
5051
import { ResponseError } from "vscode-jsonrpc";
5152
import {
@@ -70,7 +71,7 @@ import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositor
7071
import { EligibilityService } from "../user/eligibility-service";
7172
import { AccountStatementProvider } from "../user/account-statement-provider";
7273
import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol";
73-
import { BillableSession, billableSessionDummyData } from "@gitpod/gitpod-protocol/lib/usage";
74+
import { BillableSession } from "@gitpod/gitpod-protocol/lib/usage";
7475
import {
7576
AssigneeIdentityIdentifier,
7677
TeamSubscription,
@@ -106,6 +107,9 @@ import { URL } from "url";
106107
import { UserCounter } from "../user/user-counter";
107108
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
108109
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
110+
import { CachingUsageServiceClientProvider } from "@gitpod/usage-api/lib/usage/v1/sugar";
111+
import * as usage from "@gitpod/usage-api/lib/usage/v1/usage_pb";
112+
import { billableSessionDummyData } from "@gitpod/gitpod-protocol/lib/usage";
109113

110114
@injectable()
111115
export class GitpodServerEEImpl extends GitpodServerImpl {
@@ -145,6 +149,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
145149

146150
@inject(UserService) protected readonly userService: UserService;
147151

152+
@inject(CachingUsageServiceClientProvider)
153+
protected readonly usageServiceClientProvider: CachingUsageServiceClientProvider;
154+
148155
initialize(
149156
client: GitpodClient | undefined,
150157
user: User | undefined,
@@ -2064,7 +2071,11 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
20642071

20652072
await this.guardCostCenterAccess(ctx, user.id, attributionId, "get");
20662073

2067-
return billableSessionDummyData;
2074+
const usageClient = this.usageServiceClientProvider.getDefault();
2075+
const response = await usageClient.getBilledUsage(ctx, attributionId);
2076+
const sessions = response.getSessionsList().map((s) => this.mapBilledSession(s));
2077+
2078+
return sessions.concat(billableSessionDummyData); // to at least return some data for testing
20682079
}
20692080

20702081
protected async guardCostCenterAccess(
@@ -2102,6 +2113,29 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
21022113

21032114
await this.guardAccess({ kind: "costCenter", /*subject: costCenter,*/ owner }, operation);
21042115
}
2116+
2117+
protected mapBilledSession(s: usage.BilledSession): BillableSession {
2118+
function mandatory<T>(v: T, m: (v: T) => string = (s) => "" + s): string {
2119+
if (!v) {
2120+
throw new Error(`Empty value in usage.BilledSession for instanceId '${s.getInstanceId()}'`);
2121+
}
2122+
return m(v);
2123+
}
2124+
return {
2125+
attributionId: mandatory(s.getAttributionId()),
2126+
userId: s.getUserId() || undefined,
2127+
teamId: s.getTeamId() || undefined,
2128+
projectId: s.getProjectId() || undefined,
2129+
workspaceId: mandatory(s.getWorkspaceId()),
2130+
instanceId: mandatory(s.getInstanceId()),
2131+
workspaceType: mandatory(s.getWorkspaceType()) as WorkspaceType,
2132+
workspaceClass: mandatory(s.getWorkspaceClass()),
2133+
startTime: mandatory(s.getStartTime(), (t) => t!.toDate().toISOString()),
2134+
endTime: mandatory(s.getEndTime(), (t) => t!.toDate().toISOString()),
2135+
credits: s.getCredits(), // optional
2136+
};
2137+
}
2138+
21052139
// (SaaS) – admin
21062140
async adminGetAccountStatement(ctx: TraceContext, userId: string): Promise<AccountStatement> {
21072141
traceAPIParams(ctx, { userId });

components/server/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@gitpod/image-builder": "0.1.5",
3737
"@gitpod/licensor": "0.1.5",
3838
"@gitpod/supervisor-api-grpcweb": "0.1.5",
39+
"@gitpod/usage-api": "0.1.5",
3940
"@gitpod/ws-manager": "0.1.5",
4041
"@google-cloud/storage": "^5.6.0",
4142
"@improbable-eng/grpc-web-node-http-transport": "^0.14.0",

components/server/src/config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ export interface ConfigSerialized {
149149
*/
150150
imageBuilderAddr: string;
151151

152+
/**
153+
* The address usage service clients connect to
154+
* Example: usage:8080
155+
*/
156+
usageServiceAddr: string;
157+
152158
codeSync: CodeSyncConfig;
153159

154160
vsxRegistryUrl: string;

components/server/src/container-module.ts

+14
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ import { InstallationAdminTelemetryDataProvider } from "./installation-admin/tel
100100
import { IDEService } from "./ide-service";
101101
import { LicenseEvaluator } from "@gitpod/licensor/lib";
102102
import { WorkspaceClusterImagebuilderClientProvider } from "./workspace/workspace-cluster-imagebuilder-client-provider";
103+
import {
104+
CachingUsageServiceClientProvider,
105+
UsageServiceClientCallMetrics,
106+
UsageServiceClientConfig,
107+
UsageServiceClientProvider,
108+
} from "@gitpod/usage-api/lib/usage/v1/sugar";
103109

104110
export const productionContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
105111
bind(Config).toConstantValue(ConfigFile.fromFile());
@@ -247,4 +253,12 @@ export const productionContainerModule = new ContainerModule((bind, unbind, isBo
247253
bind(ProjectsService).toSelf().inSingletonScope();
248254

249255
bind(NewsletterSubscriptionController).toSelf().inSingletonScope();
256+
257+
bind(UsageServiceClientConfig).toDynamicValue((ctx) => {
258+
const config = ctx.container.get<Config>(Config);
259+
return { address: config.usageServiceAddr };
260+
});
261+
bind(CachingUsageServiceClientProvider).toSelf().inSingletonScope();
262+
bind(UsageServiceClientProvider).toService(CachingImageBuilderClientProvider);
263+
bind(UsageServiceClientCallMetrics).toService(IClientCallMetrics);
250264
});

install/installer/pkg/components/server/configmap.go

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"regexp"
1010

1111
"github.com/gitpod-io/gitpod/installer/pkg/common"
12+
"github.com/gitpod-io/gitpod/installer/pkg/components/usage"
1213
"github.com/gitpod-io/gitpod/installer/pkg/components/workspace"
1314
"github.com/gitpod-io/gitpod/installer/pkg/config/v1/experimental"
1415
corev1 "k8s.io/api/core/v1"
@@ -222,6 +223,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
222223
},
223224
ContentServiceAddr: "content-service:8080",
224225
ImageBuilderAddr: "image-builder-mk3:8080",
226+
UsageServiceAddr: fmt.Sprintf("%s:%d", usage.Component, usage.GRPCServicePort),
225227
CodeSync: CodeSync{},
226228
VSXRegistryUrl: fmt.Sprintf("https://open-vsx.%s", ctx.Config.Domain), // todo(sje): or "https://{{ .Values.vsxRegistry.host | default "open-vsx.org" }}" if not using OpenVSX proxy
227229
EnablePayment: chargebeeSecret != "" || stripeSecret != "" || stripeConfig != "",

install/installer/pkg/components/server/types.go

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type ConfigSerialized struct {
3030
RunDbDeleter bool `json:"runDbDeleter"`
3131
ContentServiceAddr string `json:"contentServiceAddr"`
3232
ImageBuilderAddr string `json:"imageBuilderAddr"`
33+
UsageServiceAddr string `json:"usageServiceAddr"`
3334
VSXRegistryUrl string `json:"vsxRegistryUrl"`
3435
ChargebeeProviderOptionsFile string `json:"chargebeeProviderOptionsFile"`
3536
StripeSecretsFile string `json:"stripeSecretsFile"`

install/installer/pkg/components/usage/constants.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const (
88
Component = "usage"
99
gRPCContainerPort = 9001
1010
gRPCPortName = "grpc"
11-
gRPCServicePort = 9001
11+
GRPCServicePort = 9001
1212
stripeSecretMountPath = "stripe-secret"
1313
stripeKeyFilename = "apikeys"
1414
configJSONFilename = "config.json"

install/installer/pkg/components/usage/service.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func service(ctx *common.RenderContext) ([]runtime.Object, error) {
1313
{
1414
Name: gRPCPortName,
1515
ContainerPort: gRPCContainerPort,
16-
ServicePort: gRPCServicePort,
16+
ServicePort: GRPCServicePort,
1717
},
1818
})(ctx)
1919
}

0 commit comments

Comments
 (0)