4
4
* See License.AGPL.txt in the project root for license information.
5
5
*/
6
6
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" ;
8
9
import { expressConnectMiddleware } from "@bufbuild/connect-express" ;
9
10
import { log } from "@gitpod/gitpod-protocol/lib/util/logging" ;
10
11
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb" ;
@@ -22,6 +23,11 @@ import { APIStatsService } from "./stats";
22
23
import { APITeamsService } from "./teams" ;
23
24
import { APIUserService } from "./user" ;
24
25
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
+ }
25
31
26
32
@injectable ( )
27
33
export class API {
@@ -68,25 +74,11 @@ export class API {
68
74
}
69
75
70
76
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
- } ;
85
77
app . use (
86
78
expressConnectMiddleware ( {
87
79
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 ) ) ) ;
90
82
}
91
83
} ,
92
84
} ) ,
@@ -96,15 +88,73 @@ export class API {
96
88
/**
97
89
* intercept handles cross-cutting concerns for all calls:
98
90
* - authentication
91
+ * - server-side observability
99
92
* TODO(ak):
100
- * - server-side observability (SLOs)
101
93
* - rate limitting
102
94
* - logging context
103
95
* - tracing
96
+ *
97
+ * - add SLOs
104
98
*/
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
+ } ;
108
158
}
109
159
110
160
private async verify ( context : HandlerContext ) {
0 commit comments