Path: blob/main/components/gitpod-protocol/src/util/tracing.ts
2500 views
/**1* Copyright (c) 2020 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import * as opentracing from "opentracing";7import { TracingConfig, initTracerFromEnv } from "jaeger-client";8import { Sampler, SamplingDecision } from "./jaeger-client-types";9import { initGlobalTracer } from "opentracing";10import { injectable } from "inversify";11import { log, LogContext } from "./logging";1213export interface TraceContext {14span?: opentracing.Span;15// TODO(gpl) We are missing this method from type opentracing.SpanContext, which breaks our code under some circumstances (testing).16// 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 forward17isDebugIDContainerOnly?: () => boolean;18}19export type TraceContextWithSpan = TraceContext & {20span: opentracing.Span;21};2223export namespace TraceContext {24export function startSpan(operation: string, parentCtx?: TraceContext): opentracing.Span {25const options: opentracing.SpanOptions = {};2627// references should contain span id.28// cf. https://github.com/jaegertracing/jaeger-client-node/issues/43229if (!!parentCtx?.span) {30const ctx = parentCtx?.span?.context();31if (ctx && !!ctx.toTraceId() && !!ctx.toSpanId()) {32options.references = [opentracing.followsFrom(ctx)];33}34}3536return opentracing.globalTracer().startSpan(operation, options);37}3839export function childContext(operation: string, parentCtx: TraceContext): TraceContextWithSpan {40const span = startSpan(operation, parentCtx);41return { span };42}4344export function withSpan(operation: string, callback: (ctx: TraceContext) => void, ctx?: TraceContext): void {45// if we don't have a parent span, don't create a trace here as those <trace-without-root-spans> are not useful.46if (!ctx || !ctx.span || !ctx.span.context()) {47callback({});48return;49}5051const span = TraceContext.startSpan(operation, ctx);52try {53callback({ span });54} catch (e) {55TraceContext.setError({ span }, e);56throw e;57} finally {58span.finish();59}60}6162export function setError(ctx: TraceContext, err: any) {63if (!ctx.span) {64return;65}6667TraceContext.addNestedTags(ctx, {68error: {69message: err.message,70stacktrace: err.stack,71},72});73ctx.span.setTag("error", true);74}7576export function setJsonRPCMetadata(ctx: TraceContext, method?: string) {77if (!ctx.span) {78return;79}8081const tags: { [key: string]: any } = {82rpc: {83system: "jsonrpc",84// version,85},86};87if (method) {88tags.rpc.method = method;89}90addNestedTags(ctx, tags);91}9293export function setJsonRPCError(94ctx: TraceContext,95method: string,96err: Error & { code: number },97withStatusCode: boolean = false,98) {99if (!ctx.span) {100return;101}102// not use setError bc this is (most likely) a working operation103104setJsonRPCMetadata(ctx, method);105// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#json-rpc106addNestedTags(ctx, {107rpc: {108jsonrpc: {109error_code: err.code,110error_message: err.message,111},112},113});114115// the field "status_code" is used by honeycomb to derive insights like success rate, etc. Defaults to "0".116if (withStatusCode) {117ctx.span.setTag("status_code", err.code);118}119}120121export function addJsonRPCParameters(ctx: TraceContext, params: { [key: string]: any }) {122if (!ctx.span) {123return;124}125126setJsonRPCMetadata(ctx);127addNestedTags(ctx, {128rpc: {129jsonrpc: {130parameters: params,131},132},133});134}135136/**137* Does what one would expect from `span.addTags`: Calls `span.addTag` for all keys in map, recursively for objects.138* Example:139* ```140* TraceContext.addNestedTags(ctx, {141* rpc: {142* system: "jsonrpc",143* jsonrpc: {144* version: "1.0",145* method: "test",146* parameters: ["abc", "def"],147* },148* },149* });150* ```151* gives152* rpc.system = "jsonrpc"153* rpc.jsonrpc.version = "1.0"154* rpc.jsonrpc.method = "test"155* rpc.jsonrpc.parameters.0 = "abc"156* rpc.jsonrpc.parameters.1 = "def"157* @param ctx158* @param keyValueMap159* @returns160*/161export function addNestedTags(ctx: TraceContext, keyValueMap: { [key: string]: any }, _namespace?: string) {162if (!ctx.span) {163return;164}165const namespace = _namespace ? `${_namespace}.` : "";166167try {168for (const k of Object.keys(keyValueMap)) {169const v = keyValueMap[k];170if (v instanceof Object) {171// eslint-disable-next-line @typescript-eslint/no-unsafe-argument172addNestedTags(ctx, v, `${namespace}${k}`);173} else {174ctx.span.setTag(`${namespace}${k}`, v);175}176}177} catch (err) {178// general resilience against odd shapes/parameters179log.error("Tracing.addNestedTags", err, { namespace });180}181}182183export function setOWI(ctx: TraceContext, owi: LogContext) {184if (!ctx.span) {185return;186}187addNestedTags(ctx, {188context: owi,189});190}191192export function finishOnce(span: opentracing.Span): () => void {193let done = false;194return () => {195if (done) return;196span.finish();197done = true;198};199}200}201202@injectable()203export class TracingManager {204public setup(serviceName: string, opts?: CustomTracerOpts) {205initGlobalTracer(this.getTracerForService(serviceName, opts));206}207208public getTracerForService(serviceName: string, opts?: CustomTracerOpts) {209const config: TracingConfig = {210disable: false,211reporter: {212logSpans: false,213},214serviceName,215};216const t = initTracerFromEnv(config, {217logger: console,218tags: {219"service.build.commit": process.env.GITPOD_BUILD_GIT_COMMIT,220"service.build.version": process.env.GITPOD_BUILD_VERSION,221},222});223224if (opts) {225if (opts.perOpSampling) {226// eslint-disable-next-line @typescript-eslint/no-unsafe-argument227(t as any)._sampler = new PerOperationSampler((t as any)._sampler, opts.perOpSampling);228}229}230return t;231}232}233234export interface CustomTracerOpts {235perOpSampling?: PerOperationSampling;236}237238// has to conform to https://github.com/jaegertracing/jaeger-client-node/blob/0042b1c0a0796bb655eb93e77ff76ab5e94c2bb6/src/_flow/sampler-thrift.js#L32239export interface PerOperationSampling {240[key: string]: boolean;241}242243export class PerOperationSampler implements Sampler {244constructor(protected readonly fallback: Sampler, protected readonly strategies: PerOperationSampling) {}245246name(): string {247return "PerOperationSampler";248}249250toString(): string {251return `${this.name()}`;252}253254isSampled(operation: string, tags: any): boolean {255const shouldSample = this.strategies[operation];256if (shouldSample === undefined) {257if (!this.fallback.isSampled) {258return false;259}260return this.fallback.isSampled(operation, tags);261}262263return shouldSample;264}265266onCreateSpan(span: opentracing.Span): SamplingDecision {267const outTags = {};268// eslint-disable-next-line @typescript-eslint/no-unsafe-argument269const isSampled = this.isSampled((span as any).operationName, outTags);270// NB: return retryable=true here since we can change decision after setOperationName().271return { sample: isSampled, retryable: true, tags: outTags };272}273274onSetOperationName(span: opentracing.Span, operationName: string): SamplingDecision {275const outTags = {};276// eslint-disable-next-line @typescript-eslint/no-unsafe-argument277const isSampled = this.isSampled((span as any).operationName, outTags);278return { sample: isSampled, retryable: false, tags: outTags };279}280281onSetTag(span: opentracing.Span, key: string, value: any): SamplingDecision {282return { sample: false, retryable: true, tags: null };283}284285equal(other: Sampler): boolean {286return false; // TODO equal should be removed287}288289close(callback: () => void): void {290// all nested samplers are of simple types, so we do not need to Close them291if (callback) {292callback();293}294}295}296297// Augment interfaces with an leading parameter "TraceContext" on every method298type IsValidArg<T> = T extends object ? (keyof T extends never ? false : true) : true;299type AddTraceContext<T> = T extends (a: infer A, b: infer B, c: infer C, d: infer D, e: infer E, f: infer F) => infer R300? IsValidArg<F> extends true301? (ctx: TraceContextWithSpan, a: A, b: B, c: C, d: D, e: E, f: F) => R302: IsValidArg<E> extends true303? (ctx: TraceContextWithSpan, a: A, b: B, c: C, d: D, e: E) => R304: IsValidArg<D> extends true305? (ctx: TraceContextWithSpan, a: A, b: B, c: C, d: D) => R306: IsValidArg<C> extends true307? (ctx: TraceContextWithSpan, a: A, b: B, c: C) => R308: IsValidArg<B> extends true309? (ctx: TraceContextWithSpan, a: A, b: B) => R310: IsValidArg<A> extends true311? (ctx: TraceContextWithSpan, a: A) => R312: (ctx: TraceContextWithSpan) => Promise<R>313: never;314315export type InterfaceWithTraceContext<T> = {316[P in keyof T]: AddTraceContext<T[P]>;317};318319320