-
Notifications
You must be signed in to change notification settings - Fork 0
/
clues.go
266 lines (227 loc) · 8.58 KB
/
clues.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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package clues
import (
"context"
"fmt"
"github.com/alcionai/clues/internal/node"
"github.com/alcionai/clues/internal/stringify"
"go.opentelemetry.io/otel/trace"
)
// ---------------------------------------------------------------------------
// persistent client initialization
// ---------------------------------------------------------------------------
// InitializeOTEL will spin up the OTEL clients that are held by clues,
// Clues will eagerly use these clients in the background to provide
// additional telemetry hook-ins.
//
// Clues will operate as expected in the event of an error, or if OTEL is not
// initialized. This is a purely optional step.
func InitializeOTEL(
ctx context.Context,
serviceName string,
config OTELConfig,
) (context.Context, error) {
nc := node.FromCtx(ctx)
err := nc.InitOTEL(ctx, serviceName, config.toInternalConfig())
if err != nil {
return ctx, err
}
return node.EmbedInCtx(ctx, nc), nil
}
// Close will flush all buffered data waiting to be read. If Initialize was not
// called, this call is a no-op. Should be called in a defer after initializing.
func Close(ctx context.Context) error {
nc := node.FromCtx(ctx)
if nc.OTEL != nil {
err := nc.OTEL.Close(ctx)
if err != nil {
return fmt.Errorf("closing otel client: %w", err)
}
}
return nil
}
// Inherit propagates all clients from one context to another. This is particularly
// useful for taking an initialized context from a main() func and ensuring its clients
// are available for request-bound conetxts, such as in a http server pattern.
//
// If the 'to' context already contains an initialized client, no change is made.
// Callers can force a 'from' client to override a 'to' client by setting clobber=true.
func Inherit(
from, to context.Context,
clobber bool,
) context.Context {
fromNode := node.FromCtx(from)
toNode := node.FromCtx(to)
if to == nil {
to = context.Background()
} else if toNode.Span == nil {
// A span may already exist in the 'to' context thanks to otel package integration.
// Likewise, the 'from' ctx is not expected to contain a span, so we only want to
// maintain the span information that's currently live.
toNode.Span = trace.SpanFromContext(to)
}
// if we have no fromNode OTEL, or are not clobbering, return the toNode.
if fromNode.OTEL == nil || (toNode.OTEL != nil && !clobber) {
return node.EmbedInCtx(to, toNode)
}
// otherwise pass along the fromNode OTEL client.
toNode.OTEL = fromNode.OTEL
return node.EmbedInCtx(to, toNode)
}
// ---------------------------------------------------------------------------
// data access
// ---------------------------------------------------------------------------
// In retrieves the clues structured data from the context.
func In(ctx context.Context) *node.Node {
return node.FromCtx(ctx)
}
// ---------------------------------------------------------------------------
// key-value metadata
// ---------------------------------------------------------------------------
// Add adds all key-value pairs to the clues.
func Add(ctx context.Context, kvs ...any) context.Context {
nc := node.FromCtx(ctx)
return node.EmbedInCtx(ctx, nc.AddValues(stringify.Normalize(kvs...)))
}
// AddMap adds a shallow clone of the map to a namespaced set of clues.
func AddMap[K comparable, V any](
ctx context.Context,
m map[K]V,
) context.Context {
nc := node.FromCtx(ctx)
kvs := make([]any, 0, len(m)*2)
for k, v := range m {
kvs = append(kvs, k, v)
}
return node.EmbedInCtx(ctx, nc.AddValues(stringify.Normalize(kvs...)))
}
// ---------------------------------------------------------------------------
// spans and traces
// ---------------------------------------------------------------------------
// InjectTrace adds the current trace details to the provided
// headers. If otel is not initialized, no-ops.
//
// The mapCarrier is mutated by this request. The passed
// reference is returned mostly as a quality-of-life step
// so that callers don't need to declare the map outside of
// this call.
func InjectTrace[C node.TraceMapCarrierBase](
ctx context.Context,
mapCarrier C,
) C {
node.FromCtx(ctx).
InjectTrace(ctx, node.AsTraceMapCarrier(mapCarrier))
return mapCarrier
}
// ReceiveTrace extracts the current trace details from the
// headers and adds them to the context. If otel is not
// initialized, no-ops.
func ReceiveTrace[C node.TraceMapCarrierBase](
ctx context.Context,
mapCarrier C,
) context.Context {
return node.FromCtx(ctx).
ReceiveTrace(ctx, node.AsTraceMapCarrier(mapCarrier))
}
// AddSpan stacks a clues node onto this context and uses the provided
// name for the trace id, instead of a randomly generated hash. AddSpan
// can be called without additional values if you only want to add a trace
// marker. The assumption is that an otel span is generated and attached
// to the node. Callers should always follow this addition with a closing
// `defer clues.CloseSpan(ctx)`.
func AddSpan(
ctx context.Context,
name string,
kvs ...any,
) context.Context {
nc := node.FromCtx(ctx)
var spanned *node.Node
if len(kvs) > 0 {
ctx, spanned = nc.AddSpan(ctx, name)
spanned.ID = name
spanned = spanned.AddValues(stringify.Normalize(kvs...))
} else {
ctx, spanned = nc.AddSpan(ctx, name)
spanned = spanned.AppendToTree(name)
}
return node.EmbedInCtx(ctx, spanned)
}
// CloseSpan closes the current span in the clues node. Should only be called
// following a `clues.AddSpan()` call.
func CloseSpan(ctx context.Context) context.Context {
return node.EmbedInCtx(
ctx,
node.FromCtx(ctx).CloseSpan(ctx))
}
// ---------------------------------------------------------------------------
// comments
// ---------------------------------------------------------------------------
// AddComment adds a long form comment to the clues.
//
// Comments are special case additions to the context. They're here to, well,
// let you add comments! Why? Because sometimes it's not sufficient to have a
// log let you know that a line of code was reached. Even a bunch of clues to
// describe system state may not be enough. Sometimes what you need in order
// to debug the situation is a long-form explanation (you do already add that
// to your code, don't you?). Or, even better, a linear history of long-form
// explanations, each one building on the prior (which you can't easily do in
// code).
//
// Should you transfer all your comments to clues? Absolutely not. But in
// cases where extra explantion is truly important to debugging production,
// when all you've got are some logs and (maybe if you're lucky) a span trace?
// Those are the ones you want.
//
// Unlike other additions, which are added as top-level key:value pairs to the
// context, comments are all held as a single array of additions, persisted in
// order of appearance, and prefixed by the file and line in which they appeared.
// This means comments are always added to the context and never clobber each
// other, regardless of their location. IE: don't add them to a loop.
func AddComment(
ctx context.Context,
msg string,
vs ...any,
) context.Context {
nc := node.FromCtx(ctx)
nn := nc.AddComment(1, msg, vs...)
return node.EmbedInCtx(ctx, nn)
}
// ---------------------------------------------------------------------------
// agents
// ---------------------------------------------------------------------------
// AddAgent adds an agent with a given name to the context. What's an agent?
// It's a special case data adder that you can spawn to collect clues for
// you. Unlike standard clues additions, you have to tell the agent exactly
// what data you want it to Relay() for you.
//
// Agents are recorded in the current clues node and all of its descendants.
// Data relayed by the agent will appear as part of the standard data map,
// namespaced by each agent.
//
// Agents are specifically handy in a certain set of uncommon cases where
// retrieving clues is otherwise difficult to do, such as working with
// middleware that doesn't allow control over error creation. In these cases
// your only option is to relay that data back to some prior clues node.
func AddAgent(
ctx context.Context,
name string,
) context.Context {
nc := node.FromCtx(ctx)
nn := nc.AddAgent(name)
return node.EmbedInCtx(ctx, nn)
}
// Relay adds all key-value pairs to the provided agent. The agent will
// record those values to the node in which it was created. All relayed
// values are namespaced to the owning agent.
func Relay(
ctx context.Context,
agent string,
vs ...any,
) {
nc := node.FromCtx(ctx)
ag, ok := nc.Agents[agent]
if !ok {
return
}
// set values, not add. We don't want agents to own a full clues tree.
ag.Data.SetValues(stringify.Normalize(vs...))
}