Skip to content

Commit 207d6e3

Browse files
committed
add server side observability
1 parent aaf34cd commit 207d6e3

File tree

2 files changed

+84
-21
lines changed

2 files changed

+84
-21
lines changed

components/server/src/api/server.ts

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { Code, ConnectError, ConnectRouter, HandlerContext } from "@bufbuild/connect";
7+
import { Code, ConnectError, ConnectRouter, HandlerContext, ServiceImpl } from "@bufbuild/connect";
8+
import { ServiceType, MethodKind } from "@bufbuild/protobuf";
89
import { expressConnectMiddleware } from "@bufbuild/connect-express";
910
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
1011
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb";
@@ -22,6 +23,11 @@ import { APIStatsService } from "./stats";
2223
import { APITeamsService } from "./teams";
2324
import { APIUserService } from "./user";
2425
import { APIWorkspacesService } from "./workspaces";
26+
import { connectServerHandled, connectServerStarted } from "../prometheus-metrics";
27+
28+
function service<T extends ServiceType>(type: T, impl: ServiceImpl<T>): [T, ServiceImpl<T>] {
29+
return [type, impl];
30+
}
2531

2632
@injectable()
2733
export class API {
@@ -68,25 +74,11 @@ export class API {
6874
}
6975

7076
private register(app: express.Application) {
71-
const self = this;
72-
const serviceInterceptor: ProxyHandler<any> = {
73-
get(target, prop, receiver) {
74-
const original = target[prop as any];
75-
if (typeof original !== "function") {
76-
return Reflect.get(target, prop, receiver);
77-
}
78-
return async (...args: any[]) => {
79-
const context = args[1] as HandlerContext;
80-
await self.intercept(context);
81-
return original.apply(target, args);
82-
};
83-
},
84-
};
8577
app.use(
8678
expressConnectMiddleware({
8779
routes: (router: ConnectRouter) => {
88-
for (const service of [this.apiHelloService]) {
89-
router.service(HelloService, new Proxy(service, serviceInterceptor));
80+
for (const [type, impl] of [service(HelloService, this.apiHelloService)]) {
81+
router.service(HelloService, new Proxy(impl, this.interceptService(type)));
9082
}
9183
},
9284
}),
@@ -96,15 +88,73 @@ export class API {
9688
/**
9789
* intercept handles cross-cutting concerns for all calls:
9890
* - authentication
91+
* - server-side observability
9992
* TODO(ak):
100-
* - server-side observability (SLOs)
10193
* - rate limitting
10294
* - logging context
10395
* - tracing
96+
*
97+
* - add SLOs
10498
*/
105-
private async intercept(context: HandlerContext): Promise<void> {
106-
const user = await this.verify(context);
107-
context.user = user;
99+
100+
private interceptService<T extends ServiceType>(type: T): ProxyHandler<ServiceImpl<T>> {
101+
const self = this;
102+
return {
103+
get(target, prop) {
104+
return async (...args: any[]) => {
105+
const method = type.methods[prop as any];
106+
if (!method) {
107+
// Increment metrics for unknown method attempts
108+
console.warn("public api: unknown method", type.typeName, prop);
109+
const code = Code.InvalidArgument;
110+
connectServerStarted.labels(type.typeName, "unknown", "unknown").inc();
111+
connectServerHandled
112+
.labels(type.typeName, "unknown", "unknown", Code[code].toLowerCase())
113+
.observe(0);
114+
throw new ConnectError("Invalid method", code);
115+
}
116+
let kind = "unknown";
117+
if (method.kind === MethodKind.Unary) {
118+
kind = "unary";
119+
} else if (method.kind === MethodKind.ServerStreaming) {
120+
kind = "server_stream";
121+
} else if (method.kind === MethodKind.ClientStreaming) {
122+
kind = "client_stream";
123+
} else if (method.kind === MethodKind.BiDiStreaming) {
124+
kind = "bidi";
125+
}
126+
127+
const context = args[1] as HandlerContext;
128+
129+
const startTime = Date.now();
130+
connectServerStarted.labels(type.typeName, method.name, kind).inc();
131+
132+
let result: any;
133+
let error: ConnectError | undefined;
134+
try {
135+
const user = await self.verify(context);
136+
context.user = user;
137+
result = await (target[prop as any] as Function).apply(target, args);
138+
} catch (e) {
139+
if (!(e instanceof ConnectError)) {
140+
console.error("public api: internal: failed to handle request", e);
141+
error = new ConnectError("internal", Code.Internal);
142+
} else {
143+
error = e;
144+
}
145+
}
146+
147+
const code = error ? Code[error.code].toLowerCase() : "ok";
148+
connectServerHandled
149+
.labels(type.typeName, method.name, kind, code)
150+
.observe((Date.now() - startTime) / 1000);
151+
if (error) {
152+
throw error;
153+
}
154+
return result;
155+
};
156+
},
157+
};
108158
}
109159

110160
private async verify(context: HandlerContext) {

components/server/src/prometheus-metrics.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,21 @@ export function registerServerMetrics(registry: prometheusClient.Registry) {
3434
registry.registerMetric(updateSubscribersRegistered);
3535
registry.registerMetric(dbConnectionsTotal);
3636
registry.registerMetric(dbConnectionsFree);
37+
registry.registerMetric(connectServerStarted);
38+
registry.registerMetric(connectServerHandled);
3739
}
3840

41+
export const connectServerStarted = new prometheusClient.Counter({
42+
name: "connect_server_started_total",
43+
help: "Counter of server connect (gRPC/HTTP) requests started",
44+
labelNames: ["package", "call", "call_type"],
45+
});
46+
export const connectServerHandled = new prometheusClient.Histogram({
47+
name: "connect_server_handled_seconds",
48+
help: "Histogram of response latency (seconds) of server connect (gRPC/HTTP) requests",
49+
labelNames: ["package", "call", "call_type", "code"],
50+
});
51+
3952
export const dbConnectionsTotal = new prometheusClient.Gauge({
4053
name: "gitpod_typeorm_total_connections",
4154
help: "Total number of connections in TypeORM pool",

0 commit comments

Comments
 (0)