Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-protocol/src/util/tracing.ts
2500 views
1
/**
2
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import * as opentracing from "opentracing";
8
import { TracingConfig, initTracerFromEnv } from "jaeger-client";
9
import { Sampler, SamplingDecision } from "./jaeger-client-types";
10
import { initGlobalTracer } from "opentracing";
11
import { injectable } from "inversify";
12
import { log, LogContext } from "./logging";
13
14
export interface TraceContext {
15
span?: opentracing.Span;
16
// TODO(gpl) We are missing this method from type opentracing.SpanContext, which breaks our code under some circumstances (testing).
17
// We should add it, but I won't add right now because of different focus, and it's unclear how we want to use tracing going forward
18
isDebugIDContainerOnly?: () => boolean;
19
}
20
export type TraceContextWithSpan = TraceContext & {
21
span: opentracing.Span;
22
};
23
24
export namespace TraceContext {
25
export function startSpan(operation: string, parentCtx?: TraceContext): opentracing.Span {
26
const options: opentracing.SpanOptions = {};
27
28
// references should contain span id.
29
// cf. https://github.com/jaegertracing/jaeger-client-node/issues/432
30
if (!!parentCtx?.span) {
31
const ctx = parentCtx?.span?.context();
32
if (ctx && !!ctx.toTraceId() && !!ctx.toSpanId()) {
33
options.references = [opentracing.followsFrom(ctx)];
34
}
35
}
36
37
return opentracing.globalTracer().startSpan(operation, options);
38
}
39
40
export function childContext(operation: string, parentCtx: TraceContext): TraceContextWithSpan {
41
const span = startSpan(operation, parentCtx);
42
return { span };
43
}
44
45
export function withSpan(operation: string, callback: (ctx: TraceContext) => void, ctx?: TraceContext): void {
46
// if we don't have a parent span, don't create a trace here as those <trace-without-root-spans> are not useful.
47
if (!ctx || !ctx.span || !ctx.span.context()) {
48
callback({});
49
return;
50
}
51
52
const span = TraceContext.startSpan(operation, ctx);
53
try {
54
callback({ span });
55
} catch (e) {
56
TraceContext.setError({ span }, e);
57
throw e;
58
} finally {
59
span.finish();
60
}
61
}
62
63
export function setError(ctx: TraceContext, err: any) {
64
if (!ctx.span) {
65
return;
66
}
67
68
TraceContext.addNestedTags(ctx, {
69
error: {
70
message: err.message,
71
stacktrace: err.stack,
72
},
73
});
74
ctx.span.setTag("error", true);
75
}
76
77
export function setJsonRPCMetadata(ctx: TraceContext, method?: string) {
78
if (!ctx.span) {
79
return;
80
}
81
82
const tags: { [key: string]: any } = {
83
rpc: {
84
system: "jsonrpc",
85
// version,
86
},
87
};
88
if (method) {
89
tags.rpc.method = method;
90
}
91
addNestedTags(ctx, tags);
92
}
93
94
export function setJsonRPCError(
95
ctx: TraceContext,
96
method: string,
97
err: Error & { code: number },
98
withStatusCode: boolean = false,
99
) {
100
if (!ctx.span) {
101
return;
102
}
103
// not use setError bc this is (most likely) a working operation
104
105
setJsonRPCMetadata(ctx, method);
106
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#json-rpc
107
addNestedTags(ctx, {
108
rpc: {
109
jsonrpc: {
110
error_code: err.code,
111
error_message: err.message,
112
},
113
},
114
});
115
116
// the field "status_code" is used by honeycomb to derive insights like success rate, etc. Defaults to "0".
117
if (withStatusCode) {
118
ctx.span.setTag("status_code", err.code);
119
}
120
}
121
122
export function addJsonRPCParameters(ctx: TraceContext, params: { [key: string]: any }) {
123
if (!ctx.span) {
124
return;
125
}
126
127
setJsonRPCMetadata(ctx);
128
addNestedTags(ctx, {
129
rpc: {
130
jsonrpc: {
131
parameters: params,
132
},
133
},
134
});
135
}
136
137
/**
138
* Does what one would expect from `span.addTags`: Calls `span.addTag` for all keys in map, recursively for objects.
139
* Example:
140
* ```
141
* TraceContext.addNestedTags(ctx, {
142
* rpc: {
143
* system: "jsonrpc",
144
* jsonrpc: {
145
* version: "1.0",
146
* method: "test",
147
* parameters: ["abc", "def"],
148
* },
149
* },
150
* });
151
* ```
152
* gives
153
* rpc.system = "jsonrpc"
154
* rpc.jsonrpc.version = "1.0"
155
* rpc.jsonrpc.method = "test"
156
* rpc.jsonrpc.parameters.0 = "abc"
157
* rpc.jsonrpc.parameters.1 = "def"
158
* @param ctx
159
* @param keyValueMap
160
* @returns
161
*/
162
export function addNestedTags(ctx: TraceContext, keyValueMap: { [key: string]: any }, _namespace?: string) {
163
if (!ctx.span) {
164
return;
165
}
166
const namespace = _namespace ? `${_namespace}.` : "";
167
168
try {
169
for (const k of Object.keys(keyValueMap)) {
170
const v = keyValueMap[k];
171
if (v instanceof Object) {
172
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
173
addNestedTags(ctx, v, `${namespace}${k}`);
174
} else {
175
ctx.span.setTag(`${namespace}${k}`, v);
176
}
177
}
178
} catch (err) {
179
// general resilience against odd shapes/parameters
180
log.error("Tracing.addNestedTags", err, { namespace });
181
}
182
}
183
184
export function setOWI(ctx: TraceContext, owi: LogContext) {
185
if (!ctx.span) {
186
return;
187
}
188
addNestedTags(ctx, {
189
context: owi,
190
});
191
}
192
193
export function finishOnce(span: opentracing.Span): () => void {
194
let done = false;
195
return () => {
196
if (done) return;
197
span.finish();
198
done = true;
199
};
200
}
201
}
202
203
@injectable()
204
export class TracingManager {
205
public setup(serviceName: string, opts?: CustomTracerOpts) {
206
initGlobalTracer(this.getTracerForService(serviceName, opts));
207
}
208
209
public getTracerForService(serviceName: string, opts?: CustomTracerOpts) {
210
const config: TracingConfig = {
211
disable: false,
212
reporter: {
213
logSpans: false,
214
},
215
serviceName,
216
};
217
const t = initTracerFromEnv(config, {
218
logger: console,
219
tags: {
220
"service.build.commit": process.env.GITPOD_BUILD_GIT_COMMIT,
221
"service.build.version": process.env.GITPOD_BUILD_VERSION,
222
},
223
});
224
225
if (opts) {
226
if (opts.perOpSampling) {
227
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
228
(t as any)._sampler = new PerOperationSampler((t as any)._sampler, opts.perOpSampling);
229
}
230
}
231
return t;
232
}
233
}
234
235
export interface CustomTracerOpts {
236
perOpSampling?: PerOperationSampling;
237
}
238
239
// has to conform to https://github.com/jaegertracing/jaeger-client-node/blob/0042b1c0a0796bb655eb93e77ff76ab5e94c2bb6/src/_flow/sampler-thrift.js#L32
240
export interface PerOperationSampling {
241
[key: string]: boolean;
242
}
243
244
export class PerOperationSampler implements Sampler {
245
constructor(protected readonly fallback: Sampler, protected readonly strategies: PerOperationSampling) {}
246
247
name(): string {
248
return "PerOperationSampler";
249
}
250
251
toString(): string {
252
return `${this.name()}`;
253
}
254
255
isSampled(operation: string, tags: any): boolean {
256
const shouldSample = this.strategies[operation];
257
if (shouldSample === undefined) {
258
if (!this.fallback.isSampled) {
259
return false;
260
}
261
return this.fallback.isSampled(operation, tags);
262
}
263
264
return shouldSample;
265
}
266
267
onCreateSpan(span: opentracing.Span): SamplingDecision {
268
const outTags = {};
269
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
270
const isSampled = this.isSampled((span as any).operationName, outTags);
271
// NB: return retryable=true here since we can change decision after setOperationName().
272
return { sample: isSampled, retryable: true, tags: outTags };
273
}
274
275
onSetOperationName(span: opentracing.Span, operationName: string): SamplingDecision {
276
const outTags = {};
277
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
278
const isSampled = this.isSampled((span as any).operationName, outTags);
279
return { sample: isSampled, retryable: false, tags: outTags };
280
}
281
282
onSetTag(span: opentracing.Span, key: string, value: any): SamplingDecision {
283
return { sample: false, retryable: true, tags: null };
284
}
285
286
equal(other: Sampler): boolean {
287
return false; // TODO equal should be removed
288
}
289
290
close(callback: () => void): void {
291
// all nested samplers are of simple types, so we do not need to Close them
292
if (callback) {
293
callback();
294
}
295
}
296
}
297
298
// Augment interfaces with an leading parameter "TraceContext" on every method
299
type IsValidArg<T> = T extends object ? (keyof T extends never ? false : true) : true;
300
type AddTraceContext<T> = T extends (a: infer A, b: infer B, c: infer C, d: infer D, e: infer E, f: infer F) => infer R
301
? IsValidArg<F> extends true
302
? (ctx: TraceContextWithSpan, a: A, b: B, c: C, d: D, e: E, f: F) => R
303
: IsValidArg<E> extends true
304
? (ctx: TraceContextWithSpan, a: A, b: B, c: C, d: D, e: E) => R
305
: IsValidArg<D> extends true
306
? (ctx: TraceContextWithSpan, a: A, b: B, c: C, d: D) => R
307
: IsValidArg<C> extends true
308
? (ctx: TraceContextWithSpan, a: A, b: B, c: C) => R
309
: IsValidArg<B> extends true
310
? (ctx: TraceContextWithSpan, a: A, b: B) => R
311
: IsValidArg<A> extends true
312
? (ctx: TraceContextWithSpan, a: A) => R
313
: (ctx: TraceContextWithSpan) => Promise<R>
314
: never;
315
316
export type InterfaceWithTraceContext<T> = {
317
[P in keyof T]: AddTraceContext<T[P]>;
318
};
319
320