generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 7
/
context.go
252 lines (223 loc) · 7.77 KB
/
context.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
package rpc
import (
"context"
"fmt"
"net/http"
"connectrpc.com/connect"
"connectrpc.com/otelconnect"
"github.com/alecthomas/types/optional"
"golang.org/x/mod/semver"
"github.com/TBD54566975/ftl"
"github.com/TBD54566975/ftl/backend/schema"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/model"
"github.com/TBD54566975/ftl/internal/rpc/headers"
)
type ftlDirectRoutingKey struct{}
type ftlVerbKey struct{}
type requestIDKey struct{}
// WithDirectRouting ensures any hops in Verb routing do not redirect.
//
// This is used so that eg. calls from Drives do not create recursive loops
// when calling back to the Agent.
func WithDirectRouting(ctx context.Context) context.Context {
return context.WithValue(ctx, ftlDirectRoutingKey{}, "1")
}
// WithVerbs adds the module.verb chain from the current request to the context.
func WithVerbs(ctx context.Context, verbs []*schema.Ref) context.Context {
return context.WithValue(ctx, ftlVerbKey{}, verbs)
}
// VerbFromContext returns the current module.verb of the current request.
func VerbFromContext(ctx context.Context) (*schema.Ref, bool) {
value := ctx.Value(ftlVerbKey{})
verbs, ok := value.([]*schema.Ref)
if len(verbs) == 0 {
return nil, false
}
return verbs[len(verbs)-1], ok
}
// VerbsFromContext returns the module.verb chain of the current request.
func VerbsFromContext(ctx context.Context) ([]*schema.Ref, bool) {
value := ctx.Value(ftlVerbKey{})
verbs, ok := value.([]*schema.Ref)
return verbs, ok
}
// IsDirectRouted returns true if the incoming request should be directly
// routed and never redirected.
func IsDirectRouted(ctx context.Context) bool {
return ctx.Value(ftlDirectRoutingKey{}) != nil
}
// RequestKeyFromContext returns the request key from the context, if any.
//
// TODO: Return an Option here instead of a bool.
func RequestKeyFromContext(ctx context.Context) (optional.Option[model.RequestKey], error) {
value := ctx.Value(requestIDKey{})
keyStr, ok := value.(string)
if !ok {
return optional.None[model.RequestKey](), nil
}
key, err := model.ParseRequestKey(keyStr)
if err != nil {
return optional.None[model.RequestKey](), fmt.Errorf("invalid request key: %w", err)
}
return optional.Some(key), nil
}
// WithRequestName adds the request key to the context.
func WithRequestName(ctx context.Context, key model.RequestKey) context.Context {
return context.WithValue(ctx, requestIDKey{}, key.String())
}
func DefaultClientOptions(level log.Level) []connect.ClientOption {
interceptors := []connect.Interceptor{MetadataInterceptor(log.Debug), otelInterceptor()}
if ftl.Version != "dev" {
interceptors = append(interceptors, versionInterceptor{})
}
return []connect.ClientOption{
connect.WithGRPC(), // Use gRPC because some servers will not be using Connect.
connect.WithInterceptors(interceptors...),
}
}
func DefaultHandlerOptions() []connect.HandlerOption {
interceptors := []connect.Interceptor{MetadataInterceptor(log.Debug), otelInterceptor()}
if ftl.Version != "dev" {
interceptors = append(interceptors, versionInterceptor{})
}
return []connect.HandlerOption{connect.WithInterceptors(interceptors...)}
}
func otelInterceptor() connect.Interceptor {
otel, err := otelconnect.NewInterceptor(otelconnect.WithTrustRemote(), otelconnect.WithoutServerPeerAttributes())
if err != nil {
panic(err)
}
return otel
}
// MetadataInterceptor propagates FTL metadata through servers and clients.
//
// "errorLevel" is the level at which errors will be logged
func MetadataInterceptor(errorLevel log.Level) connect.Interceptor {
return &metadataInterceptor{
errorLevel: errorLevel,
}
}
type metadataInterceptor struct {
errorLevel log.Level
}
func (*metadataInterceptor) WrapStreamingClient(req connect.StreamingClientFunc) connect.StreamingClientFunc {
return func(ctx context.Context, s connect.Spec) connect.StreamingClientConn {
// TODO(aat): I can't figure out how to get the client headers here.
logger := log.FromContext(ctx)
logger.Tracef("%s (streaming client)", s.Procedure)
return req(ctx, s)
}
}
func (m *metadataInterceptor) WrapStreamingHandler(req connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
return func(ctx context.Context, s connect.StreamingHandlerConn) error {
logger := log.FromContext(ctx)
logger.Tracef("%s (streaming handler)", s.Spec().Procedure)
ctx, err := propagateHeaders(ctx, s.Spec().IsClient, s.RequestHeader())
if err != nil {
return err
}
err = req(ctx, s)
if err != nil {
if connect.CodeOf(err) == connect.CodeCanceled {
return nil
}
logger.Logf(m.errorLevel, "Streaming RPC failed: %s: %s", err, s.Spec().Procedure)
return err
}
return nil
}
}
func (m *metadataInterceptor) WrapUnary(uf connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
logger := log.FromContext(ctx)
logger.Tracef("%s (unary)", req.Spec().Procedure)
ctx, err := propagateHeaders(ctx, req.Spec().IsClient, req.Header())
if err != nil {
return nil, err
}
resp, err := uf(ctx, req)
if err != nil {
logger.Logf(m.errorLevel, "Unary RPC failed: %s: %s", err, req.Spec().Procedure)
return nil, err
}
return resp, nil
}
}
type clientKey[Client Pingable] struct{}
// ContextWithClient returns a context with an RPC client attached.
func ContextWithClient[Client Pingable](ctx context.Context, client Client) context.Context {
return context.WithValue(ctx, clientKey[Client]{}, client)
}
// ClientFromContext returns the given RPC client from the context, or panics.
func ClientFromContext[Client Pingable](ctx context.Context) Client {
value := ctx.Value(clientKey[Client]{})
if value == nil {
panic("no RPC client in context")
}
return value.(Client) //nolint:forcetypeassert
}
func IsClientAvailableInContext[Client Pingable](ctx context.Context) bool {
return ctx.Value(clientKey[Client]{}) != nil
}
func propagateHeaders(ctx context.Context, isClient bool, header http.Header) (context.Context, error) {
if isClient {
if IsDirectRouted(ctx) {
headers.SetDirectRouted(header)
}
if verbs, ok := VerbsFromContext(ctx); ok {
headers.SetCallers(header, verbs)
}
if key, err := RequestKeyFromContext(ctx); err != nil {
return nil, err
} else if key, ok := key.Get(); ok {
headers.SetRequestKey(header, key)
}
} else {
if headers.IsDirectRouted(header) {
ctx = WithDirectRouting(ctx)
}
if verbs, err := headers.GetCallers(header); err != nil {
return nil, err
} else { //nolint:revive
ctx = WithVerbs(ctx, verbs)
}
if key, ok, err := headers.GetRequestKey(header); err != nil {
return nil, err
} else if ok {
ctx = WithRequestName(ctx, key)
}
}
return ctx, nil
}
// versionInterceptor reports a warning to the client if the client is older than the server.
type versionInterceptor struct{}
func (v versionInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {
return next
}
func (v versionInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
return next
}
func (v versionInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, ar connect.AnyRequest) (connect.AnyResponse, error) {
resp, err := next(ctx, ar)
if err != nil {
return nil, err
}
if ar.Spec().IsClient {
if err := v.checkVersion(resp.Header()); err != nil {
log.FromContext(ctx).Warnf("%s", err)
}
} else {
resp.Header().Set("X-FTL-Version", ftl.Version)
}
return resp, nil
}
}
func (v versionInterceptor) checkVersion(header http.Header) error {
version := header.Get("X-FTL-Version")
if semver.Compare(ftl.Version, version) < 0 {
return fmt.Errorf("FTL client (%s) is older than server (%s), consider upgrading: https://github.com/TBD54566975/ftl/releases", ftl.Version, version)
}
return nil
}