Skip to content

Commit

Permalink
feat(SugaredTracer): add draft of sugaredTracer (#3317)
Browse files Browse the repository at this point in the history
* feat(api): add experimental package and SugaredTracer

* tests(api/experimental): add tests

* fix: packages.json exports

* move export into experimental package

* add additional tests

* fix: do not use catch and finally for compatibility with Node 8

---------

Co-authored-by: Chengzhong Wu <legendecas@gmail.com>
  • Loading branch information
secustor and legendecas committed Jan 16, 2024
1 parent 6898a34 commit 71ef1b1
Show file tree
Hide file tree
Showing 7 changed files with 440 additions and 5 deletions.
4 changes: 4 additions & 0 deletions api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

## Unreleased

* feat(api): add SugaredTracer for functions not defined in the spec

## Unreleased

## 1.7.0

### :rocket: (Enhancement)
Expand Down
14 changes: 14 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@
"./build/esnext/platform/index.js": "./build/esnext/platform/browser/index.js",
"./build/src/platform/index.js": "./build/src/platform/browser/index.js"
},
"exports": {
".": {
"module": "./build/esm/index.js",
"esnext": "./build/esnext/index.js",
"types": "./build/src/index.d.ts",
"default": "./build/src/index.js"
},
"./experimental": {
"module": "./build/esm/experimental.js",
"esnext": "./build/esnext/experimental.js",
"types": "./build/src/experimental.d.ts",
"default": "./build/src/experimental.js"
}
},
"repository": "open-telemetry/opentelemetry-js",
"scripts": {
"clean": "tsc --build --clean tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
Expand Down
17 changes: 17 additions & 0 deletions api/src/experimental/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { wrapTracer, SugaredTracer } from './trace/SugaredTracer';
export { SugaredSpanOptions } from './trace/SugaredOptions';
29 changes: 29 additions & 0 deletions api/src/experimental/trace/SugaredOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Span, SpanOptions } from '../../';

/**
* Options needed for span creation
*/
export interface SugaredSpanOptions extends SpanOptions {
/**
* function to overwrite default exception behavior to record the exception. No exceptions should be thrown in the function.
* @param e Error which triggered this exception
* @param span current span from context
*/
onException?: (e: Error, span: Span) => void;
}
215 changes: 215 additions & 0 deletions api/src/experimental/trace/SugaredTracer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SugaredSpanOptions } from './SugaredOptions';
import { context, Context, Span, SpanStatusCode, Tracer } from '../../';

const defaultOnException = (e: Error, span: Span) => {
span.recordException(e);
span.setStatus({
code: SpanStatusCode.ERROR,
});
};

/**
* return a new SugaredTracer created from the supplied one
* @param tracer
*/
export function wrapTracer(tracer: Tracer): SugaredTracer {
return new SugaredTracer(tracer);
}

export class SugaredTracer implements Tracer {
private readonly _tracer: Tracer;

constructor(tracer: Tracer) {
this._tracer = tracer;
this.startSpan = tracer.startSpan.bind(this._tracer);
this.startActiveSpan = tracer.startActiveSpan.bind(this._tracer);
}

startActiveSpan: Tracer['startActiveSpan'];
startSpan: Tracer['startSpan'];

/**
* Starts a new {@link Span} and calls the given function passing it the
* created span as first argument.
* Additionally, the new span gets set in context and this context is activated
* for the duration of the function call.
* The span will be closed after the function has executed.
* If an exception occurs, it is recorded, the status is set to ERROR and the exception is rethrown.
*
* @param name The name of the span
* @param [options] SugaredSpanOptions used for span creation
* @param [context] Context to use to extract parent
* @param fn function called in the context of the span and receives the newly created span as an argument
* @returns return value of fn
* @example
* const something = tracer.withActiveSpan('op', span => {
* // do some work
* });
* @example
* const something = await tracer.withActiveSpan('op', span => {
* // do some async work
* });
*/
withActiveSpan<F extends (span: Span) => ReturnType<F>>(
name: string,
fn: F
): ReturnType<F>;
withActiveSpan<F extends (span: Span) => ReturnType<F>>(
name: string,
options: SugaredSpanOptions,
fn: F
): ReturnType<F>;
withActiveSpan<F extends (span: Span) => ReturnType<F>>(
name: string,
options: SugaredSpanOptions,
context: Context,
fn: F
): ReturnType<F>;
withActiveSpan<F extends (span: Span) => ReturnType<F>>(
name: string,
arg2: F | SugaredSpanOptions,
arg3?: F | Context,
arg4?: F
): ReturnType<F> {
const { opts, ctx, fn } = massageParams(arg2, arg3, arg4);

return this._tracer.startActiveSpan(name, opts, ctx, (span: Span) =>
handleFn(span, opts, fn)
) as ReturnType<F>;
}

/**
* Starts a new {@link Span} and ends it after execution of fn without setting it on context.
* The span will be closed after the function has executed.
* If an exception occurs, it is recorded, the status is et to ERROR and rethrown.
*
* This method does NOT modify the current Context.
*
* @param name The name of the span
* @param [options] SugaredSpanOptions used for span creation
* @param [context] Context to use to extract parent
* @param fn function called in the context of the span and receives the newly created span as an argument
* @returns Span The newly created span
* @example
* const something = tracer.withSpan('op', span => {
* // do some work
* });
* @example
* const something = await tracer.withSpan('op', span => {
* // do some async work
* });
*/
withSpan<F extends (span: Span) => ReturnType<F>>(
name: string,
fn: F
): ReturnType<F>;
withSpan<F extends (span: Span) => ReturnType<F>>(
name: string,
options: SugaredSpanOptions,
fn: F
): ReturnType<F>;
withSpan<F extends (span: Span) => ReturnType<F>>(
name: string,
options: SugaredSpanOptions,
context: Context,
fn: F
): ReturnType<F>;
withSpan<F extends (span: Span) => ReturnType<F>>(
name: string,
options: SugaredSpanOptions,
context: Context,
fn: F
): ReturnType<F>;
withSpan<F extends (span: Span) => ReturnType<F>>(
name: string,
arg2: SugaredSpanOptions | F,
arg3?: Context | F,
arg4?: F
): ReturnType<F> {
const { opts, ctx, fn } = massageParams(arg2, arg3, arg4);

const span = this._tracer.startSpan(name, opts, ctx);
return handleFn(span, opts, fn) as ReturnType<F>;
}
}

/**
* Massages parameters of withSpan and withActiveSpan to allow signature overwrites
* @param arg
* @param arg2
* @param arg3
*/
function massageParams<F extends (span: Span) => ReturnType<F>>(
arg: F | SugaredSpanOptions,
arg2?: F | Context,
arg3?: F
) {
let opts: SugaredSpanOptions | undefined;
let ctx: Context | undefined;
let fn: F;

if (!arg2 && !arg3) {
fn = arg as F;
} else if (!arg3) {
opts = arg as SugaredSpanOptions;
fn = arg2 as F;
} else {
opts = arg as SugaredSpanOptions;
ctx = arg2 as Context;
fn = arg3 as F;
}
opts = opts ?? {};
ctx = ctx ?? context.active();

return { opts, ctx, fn };
}

/**
* Executes fn, returns results and runs onException in the case of exception to allow overwriting of error handling
* @param span
* @param opts
* @param fn
*/
function handleFn<F extends (span: Span) => ReturnType<F>>(
span: Span,
opts: SugaredSpanOptions,
fn: F
): ReturnType<F> {
const onException = opts.onException ?? defaultOnException;
const errorHandler = (e: Error) => {
onException(e, span);
span.end();
throw e;
};

try {
const ret = fn(span) as Promise<ReturnType<F>>;
// if fn is an async function, attach a recordException and spanEnd callback to the promise
if (typeof ret?.then === 'function') {
return ret.then(val => {
span.end();
return val;
}, errorHandler) as ReturnType<F>;
}
span.end();
return ret as ReturnType<F>;
} catch (e) {
// add throw to signal the compiler that this will throw in the inner scope
throw errorHandler(e);
}
}

0 comments on commit 71ef1b1

Please sign in to comment.