Skip to content
This repository has been archived by the owner on Oct 3, 2023. It is now read-only.

Refactor Tracer to check presence of Zone global variable #103

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 55 additions & 28 deletions packages/opencensus-web-core/src/trace/model/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,64 +19,87 @@ import { RootSpan } from './root-span';
import { Span } from './span';
import { TracerBase } from './tracer-base';
import { randomTraceId } from '../../common/id-util';
import { WindowWithZone } from './types';

/** Tracer manages the current root span and trace header propagation. */
export class Tracer extends TracerBase implements webTypes.Tracer {
private static singletonInstance: Tracer;

/** Gets the tracer instance. */
static get instance(): Tracer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for adding this instance getter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw that opencensus node does it and I think is more understandable that Tracer is a Singleton class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, makes sense.

return this.singletonInstance || (this.singletonInstance = new this());
}

// Variable to store current root span in case the Zone global variable is not present.
// For that case we only need to store only one current root span.
private currentRootSpanNoZone = new RootSpan(this);

/**
* Gets the current root span associated to Zone.current.
* If the current zone does not have a root span (e.g. root zone),
* create a new root.
* If the current zone does not have a root span (e.g. root zone) or
* `Zone` is not present return the value store in the unique root span.
*/
get currentRootSpan(): Span {
if (Zone.current.get('data')) {
if (this.isZonePresent() && Zone.current.get('data')) {
return Zone.current.get('data').rootSpan;
}
return new RootSpan(this);
return this.currentRootSpanNoZone;
}

/**
* Sets the current root span to the current Zone.
* If the current zone does not have a 'data' property (e.g. root zone)
* do not set the root span.
* or `Zone` is not present, just assign the root span to a variable.
*/
set currentRootSpan(root: Span) {
if (Zone.current.get('data')) {
if (this.isZonePresent() && Zone.current.get('data')) {
Zone.current.get('data')['rootSpan'] = root;
} else {
this.currentRootSpanNoZone = root as RootSpan;
}
}

/**
* Creates a new Zone and start a new RootSpan to `currentRootSpan` associating
* the new RootSpan to the new Zone. Thus, there might be several root spans
* at the same time.
* Creates a new Zone (in case `Zone` global variable is present) and start
* a new RootSpan to `currentRootSpan` associating the new RootSpan to the
* new Zone. Thus, there might be several root spans at the same time.
* If `Zone` is not present, just create the root span and store it in the current
* root span.
* Currently no sampling decisions are propagated or made here.
* @param options Options for tracer instance
* @param fn Callback function
* @returns The callback return
*/
startRootSpan<T>(options: webTypes.TraceOptions, fn: (root: Span) => T): T {
let traceId = randomTraceId();
if (options.spanContext && options.spanContext.traceId) {
traceId = options.spanContext.traceId;
}
// Create the new zone.
const zoneSpec = {
name: traceId,
properties: {
data: {
isTracingZone: true,
traceId,
if (this.isZonePresent()) {
let traceId = randomTraceId();
if (options.spanContext && options.spanContext.traceId) {
traceId = options.spanContext.traceId;
}
// Create the new zone.
const zoneSpec = {
name: traceId,
properties: {
data: {
isTracingZone: true,
traceId,
},
},
},
};
};

const newZone = Zone.current.fork(zoneSpec);
return newZone.run(() => {
super.startRootSpan(options, root => {
// Set the currentRootSpan to the new created root span.
this.currentRootSpan = root;
return fn(root);
const newZone = Zone.current.fork(zoneSpec);
return newZone.run(() => {
super.startRootSpan(options, root => {
// Set the currentRootSpan to the new created root span.
this.currentRootSpan = root;
return fn(root);
});
});
}
return super.startRootSpan(options, root => {
// Set the currentRootSpan to the new created root span.
this.currentRootSpan = root;
return fn(root);
});
}

Expand Down Expand Up @@ -108,4 +131,8 @@ export class Tracer extends TracerBase implements webTypes.Tracer {

/** Binds trace context to NodeJS event emitter. No-op for opencensus-web. */
wrapEmitter(emitter: webTypes.NodeJsEventEmitter) {}

isZonePresent(): boolean {
return !!(window as WindowWithZone).Zone;
}
}
2 changes: 1 addition & 1 deletion packages/opencensus-web-core/src/trace/model/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const NOOP_EXPORTER = new NoopExporter();
/** Main interface for tracing. */
export class Tracing implements webTypes.Tracing {
/** Object responsible for managing a trace. */
readonly tracer = new Tracer();
readonly tracer = Tracer.instance;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this similar to how OpenCensus Node does it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I saw they do it like this, and it is more understandable that this class is Singleton.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!


/** Service to send collected traces to. */
exporter = NOOP_EXPORTER;
Expand Down
20 changes: 20 additions & 0 deletions packages/opencensus-web-core/src/trace/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright 2019, OpenCensus 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
*
* http://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.
*/

/** Interface to add `Zone` variable, used to check the `Zone` presence. */
export interface WindowWithZone extends Window {
Zone?: Function;
}
140 changes: 101 additions & 39 deletions packages/opencensus-web-core/test/test-tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

import * as webTypes from '@opencensus/web-types';
import { Tracer } from '../src/trace/model/tracer';
import { WindowWithZone } from '../src/trace/model/types';

describe('Tracer', () => {
let tracer: Tracer;
let listener: webTypes.SpanEventListener;
const options = { name: 'test' };
let realZone: Function | undefined;
const windowWithZone = window as WindowWithZone;

beforeEach(() => {
tracer = new Tracer();
Expand All @@ -31,37 +34,113 @@ describe('Tracer', () => {
tracer.eventListeners = [listener];
});

/** Should get/set the current RootSpan from tracer instance */
describe('get/set currentRootSpan()', () => {
it('should get the current RootSpan from tracer instance', () => {
tracer.startRootSpan(options, root => {
expect(root).toBeTruthy();
expect(root).toBe(tracer.currentRootSpan);
describe('Zone is not present', () => {
beforeEach(() => {
realZone = windowWithZone.Zone;
windowWithZone.Zone = undefined;
});

afterEach(() => {
windowWithZone.Zone = realZone;
});

it('isZonePresent() is false', () => {
expect(tracer.isZonePresent()).toBeFalsy();
});

describe('get/set currentRootSpan()', () => {
// As `Zone` is not present, the current root span is accesible outside
// the callback as there is only one current root span.
it('should get the current RootSpan from tracer instance', () => {
const onStartFn = jasmine.createSpy('onStartFn');
tracer.startRootSpan(options, onStartFn);
const onStartRoot = onStartFn.calls.argsFor(0)[0];
expect(onStartRoot).toBe(tracer.currentRootSpan);
});
});

describe('startRootSpan', () => {
it('should create start RootSpan', () => {
const onStartFn = jasmine.createSpy('onStartFn');
const oldRoot = tracer.currentRootSpan;

tracer.startRootSpan(options, onStartFn);

expect(onStartFn).toHaveBeenCalled();
const onStartRoot = onStartFn.calls.argsFor(0)[0];
expect(onStartRoot.name).toBe('test');
expect(onStartRoot).not.toBe(oldRoot);
expect(tracer.currentRootSpan).toBe(onStartRoot);
expect(listener.onStartSpan).toHaveBeenCalledWith(onStartRoot);
});
});

describe('startChildSpan', () => {
it('starts a child span of the current root span', () => {
spyOn(tracer.currentRootSpan, 'startChildSpan');
tracer.startChildSpan({
name: 'child1',
kind: webTypes.SpanKind.CLIENT,
});
expect(tracer.currentRootSpan.startChildSpan).toHaveBeenCalledWith({
childOf: tracer.currentRootSpan,
name: 'child1',
kind: webTypes.SpanKind.CLIENT,
});
});
});
});

describe('startRootSpan', () => {
it('should create a new RootSpan instance', () => {
tracer.startRootSpan(options, rootSpan => {
expect(rootSpan).toBeTruthy();
describe('Zone is present', () => {
/** Should get/set the current RootSpan from tracer instance */
describe('get/set currentRootSpan()', () => {
it('should get the current RootSpan from tracer instance', () => {
tracer.startRootSpan(options, root => {
expect(root).toBeTruthy();
expect(root).toBe(tracer.currentRootSpan);
});
});
});
it('sets current root span', () => {
const oldRoot = tracer.currentRootSpan;

tracer.startRootSpan(options, rootSpan => {
expect(rootSpan.name).toBe('test');
expect(rootSpan).not.toBe(oldRoot);
expect(tracer.currentRootSpan).toBe(rootSpan);
expect(listener.onStartSpan).toHaveBeenCalledWith(rootSpan);
describe('startRootSpan', () => {
it('should create a new RootSpan instance', () => {
tracer.startRootSpan(options, rootSpan => {
expect(rootSpan).toBeTruthy();
});
});
it('sets current root span', () => {
const oldRoot = tracer.currentRootSpan;

tracer.startRootSpan(options, rootSpan => {
expect(rootSpan.name).toBe('test');
expect(rootSpan).not.toBe(oldRoot);
expect(tracer.currentRootSpan).toBe(rootSpan);
expect(listener.onStartSpan).toHaveBeenCalledWith(rootSpan);
});
});
it('should create a new Zone and RootSpan associated to the zone', () => {
tracer.startRootSpan(options, rootSpan => {
expect(rootSpan).toBeTruthy();
expect(Zone.current).not.toBe(Zone.root);
expect(Zone.current.get('data').rootSpan).toBe(rootSpan);
});
});
});
it('should create a new Zone and RootSpan an associated to the zone', () => {
tracer.startRootSpan(options, rootSpan => {
expect(rootSpan).toBeTruthy();
expect(Zone.current).not.toBe(Zone.root);
expect(Zone.current.get('data').rootSpan).toBe(rootSpan);

describe('startChildSpan', () => {
let rootSpanLocal: webTypes.Span;
let span: webTypes.Span;
it('starts a child span of the current root span', () => {
tracer.startRootSpan(options, rootSpan => {
rootSpanLocal = rootSpan;
span = tracer.startChildSpan({
name: 'child1',
kind: webTypes.SpanKind.CLIENT,
});
});
expect(span).toBeTruthy();
expect(rootSpanLocal.numberOfChildren).toBe(1);
expect(rootSpanLocal.spans[0]).toBe(span);
});
});
});
Expand All @@ -74,23 +153,6 @@ describe('Tracer', () => {
});
});

describe('startChildSpan', () => {
let rootSpanLocal: webTypes.Span;
let span: webTypes.Span;
it('starts a child span of the current root span', () => {
tracer.startRootSpan(options, rootSpan => {
rootSpanLocal = rootSpan;
span = tracer.startChildSpan({
name: 'child1',
kind: webTypes.SpanKind.CLIENT,
});
});
expect(span).toBeTruthy();
expect(rootSpanLocal.numberOfChildren).toBe(1);
expect(rootSpanLocal.spans[0]).toBe(span);
});
});

describe('wrap', () => {
it('just returns the given function', () => {
const wrapFn = jasmine.createSpy('wrapFn');
Expand Down