diff --git a/documentation/docs/playground.js b/documentation/docs/playground.js index bda67ddf..c179e481 100644 --- a/documentation/docs/playground.js +++ b/documentation/docs/playground.js @@ -4,7 +4,6 @@ const DataPoint = require("@data-point/core"); const DPModel = require("@data-point/core/model"); const DPIfThenElse = require("@data-point/core/ifThenElse"); const DPMap = require("@data-point/core/map"); -const DPTracer = require("@data-point/tracer"); const fs = require("fs"); @@ -70,34 +69,9 @@ async function main() { } ]; - const tracer = new DPTracer(); - - const span = tracer.startSpan("data-point-request"); - console.time("dp1"); - result = await datapoint.resolve(input, DPMap(myModel), { - // tracer: span - }); + result = await datapoint.resolve(input, DPMap(myModel)); console.timeEnd("dp1"); - - span.finish(); - - console.time("dp2"); - result = await datapoint.resolve(input, DPMap(myModel), { - // tracer: span - }); - console.timeEnd("dp2"); - - // span2.finish(); - - // fs.writeFileSync( - // "/Users/pacheca/Downloads/tracing.json", - // JSON.stringify(tracer.report("chrome-tracing")) - // ); - - console.log(tracer.report("chrome-tracing")); - console.log(store); - console.log(result); } main(); diff --git a/packages/data-point-tracer/index.js b/packages/data-point-tracer/index.js deleted file mode 100644 index abd3c934..00000000 --- a/packages/data-point-tracer/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./src/Tracer"); diff --git a/packages/data-point-tracer/package.json b/packages/data-point-tracer/package.json deleted file mode 100644 index 052a6ad2..00000000 --- a/packages/data-point-tracer/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@data-point/tracer", - "version": "0.0.0", - "description": "Simple tracer", - "main": "index.js", - "private": true, - "license": "Apache-2.0", - "homepage": "https://github.com/ViacomInc/data-point", - "repository": { - "type": "git", - "url": "https://github.com/ViacomInc/data-point.git" - }, - "bugs": { - "url": "https://github.com/ViacomInc/data-point/issues" - }, - "engines": { - "node": ">=8", - "npm": ">=2" - }, - "author": { - "name": "Acatl Pacheco", - "email": "acatl.pacheco@viacom.com" - } -} diff --git a/packages/data-point-tracer/src/Span.js b/packages/data-point-tracer/src/Span.js deleted file mode 100644 index 2dff17e8..00000000 --- a/packages/data-point-tracer/src/Span.js +++ /dev/null @@ -1,52 +0,0 @@ -function toMS(hrtime) { - const nanoseconds = hrtime[0] * 1e9 + hrtime[1]; - // return nanoseconds / 1e6; - return nanoseconds / 1000; -} - -class Span { - constructor(name, options = {}) { - this.spans = []; - this.name = name; - this.tags = new Map(); - this.__log = []; - this.hrtimeStart = process.hrtime(); - this.childOf = options.childOf; - this.tracer = null; - } - - setTag(name, value) { - this.tags.set(name, value); - return this; - } - - log(logObject) { - this.__log.push(logObject); - } - - finish() { - this.hrtimeDuration = process.hrtime(this.hrtimeStart); - } - - startSpan(name, options) { - const span = new Span(name, options); - span.tracer = this.tracer; - - span.tracer.spans.push(span); - return span; - } - - toJSON() { - return { - pid: this.tags.get("pid") || 0, - name: this.name, - start: toMS(this.hrtimeStart), - duration: toMS(this.hrtimeDuration), - tags: this.tags, - log: this.__log, - childOf: this.childOf - }; - } -} - -module.exports = Span; diff --git a/packages/data-point-tracer/src/Tracer.js b/packages/data-point-tracer/src/Tracer.js deleted file mode 100644 index 30f41889..00000000 --- a/packages/data-point-tracer/src/Tracer.js +++ /dev/null @@ -1,30 +0,0 @@ -const Span = require("./Span"); -const reporters = require("./reporters"); - -class Tracer { - constructor() { - this.spans = []; - } - - startSpan(name, options) { - const span = new Span(name, options); - span.tracer = this; - - this.spans.push(span); - return span; - } - - report(reportType) { - if (this.spans.length === 0) { - return []; - } - switch (reportType) { - case "chrome-tracing": - return reporters.chromeTracer(this.spans); - default: - return reporters.toJSON(this.spans); - } - } -} - -module.exports = Tracer; diff --git a/packages/data-point-tracer/src/reporters.js b/packages/data-point-tracer/src/reporters.js deleted file mode 100644 index 17538fca..00000000 --- a/packages/data-point-tracer/src/reporters.js +++ /dev/null @@ -1,49 +0,0 @@ -function toJSON(spans) { - return spans.map(span => { - return span.toJSON(); - }); -} - -function chromeTracer(spans) { - const firstSpan = spans[0].toJSON(); - const timeStart = firstSpan.start; - const traceEvents = spans.reduce((report, span) => { - const spanJson = span.toJSON(); - report.push( - // { - // cat: "data-point", - // pid: spanJson.pid, - // name: spanJson.name, - // ph: "b", - // ts: spanJson.start - timeStart, - // id: 0x100 - // }, - // { - // cat: "data-point", - // pid: spanJson.pid, - // name: spanJson.name, - // ph: "e", - // ts: spanJson.duration, - // id: 0x100 - // }, - { - pid: spanJson.pid, - tid: 1, - ts: spanJson.start - timeStart, - dur: spanJson.duration, - ph: "X", - name: `[${spanJson.pid}]${spanJson.name}` - } - ); - return report; - }, []); - - return { - traceEvents - }; -} - -module.exports = { - toJSON, - chromeTracer -}; diff --git a/packages/data-point/package.json b/packages/data-point/package.json index 4f30e0b1..7a4b64d5 100644 --- a/packages/data-point/package.json +++ b/packages/data-point/package.json @@ -1,6 +1,6 @@ { "name": "@data-point/core", - "version": "3.4.3", + "version": "6.0.0", "description": "Data Processing and Transformation Utility", "main": "index.js", "private": true, diff --git a/packages/data-point/src/DataPoint.js b/packages/data-point/src/DataPoint.js index c6c0ec51..1346344a 100644 --- a/packages/data-point/src/DataPoint.js +++ b/packages/data-point/src/DataPoint.js @@ -4,6 +4,8 @@ const { resolve } = require("./resolve"); const { Cache } = require("./Cache"); const isPlainObject = require("./is-plain-object"); +const Tracer = require("./tracing/Tracer"); + /** * Applies a reducer from an accumulator object. * @@ -13,7 +15,7 @@ const isPlainObject = require("./is-plain-object"); */ async function resolveFromAccumulator(acc, reducer) { const parsedReducers = createReducer(reducer); - return resolve(acc, parsedReducers); + return resolve(acc, parsedReducers, true); } /** @@ -25,8 +27,8 @@ async function resolveFromAccumulator(acc, reducer) { * @param {Cache|undefined} options.cache cache manager, see Cache for options. * @param {Object|undefined} options.locals persistent object that is * accessible via the Accumulator object on every reducer. - * @param {OpenTrace.Span|undefined} options.tracer when provided it should - * comply with the **opentracing** Span API. + * @param {Tracer} options.tracer when provided it should + * comply with the DataPoint Tracing API. * @returns {Promise} resolved value */ async function resolveFromInput(input, reducer, options = {}) { @@ -55,29 +57,22 @@ function validateLocals(locals) { } /** - * @param {OpenTrace.Span} span when provided it should - * comply with the **opentracing** Span API. - * @throws Error if the object does not expose the methods `startSpan`, - * `setTag`, `log`. + * @param {Tracer} options.tracer when provided it should + * comply with the DataPoint Tracing API. + * @throws Error if the object does not expose the methods `start`. */ -function validateTracingSpan(span) { - if (span) { - if (typeof span.startSpan !== "function") { - throw new Error( - "tracer.startSpan must be a function, tracer expects opentracing API (see https://opentracing.io)" - ); +function validateTracer(tracer) { + if (tracer) { + if (typeof tracer.start !== "function") { + throw new Error("tracer.start must be a function"); } - if (typeof span.setTag !== "function") { - throw new Error( - "tracer.setTag must be a function, tracer expects opentracing API (see https://opentracing.io)" - ); + if (tracer.error && typeof tracer.error !== "function") { + throw new Error("tracer.error must be a function"); } - if (typeof span.log !== "function") { - throw new Error( - "tracer.log must be a function, tracer expects opentracing API (see https://opentracing.io)" - ); + if (tracer.finish && typeof tracer.finish !== "function") { + throw new Error("tracer.finish must be a function"); } } } @@ -100,18 +95,24 @@ class DataPoint { * @param {Object} options * @param {Object|undefined} options.locals persistent object that is * accessible via the Accumulator object on every reducer. - * @param {OpenTrace.Span|undefined} options.tracer when provided it should - * comply with the **opentracing** Span API. + * @param {Tracer} options.tracer when provided it should + * comply with the DataPoint Tracing API. * @returns {Promise} result from running the input thru the * provided reducer. */ async resolve(input, reducer, options = {}) { validateLocals(options.locals); - validateTracingSpan(options.tracer); + validateTracer(options.tracer); + + let tracer; + if (options.tracer) { + tracer = new Tracer(options.tracer); + } return resolveFromInput(input, reducer, { ...options, - cache: this.cache + cache: this.cache, + tracer }); } } @@ -121,5 +122,5 @@ module.exports = { resolveFromInput, DataPoint, validateLocals, - validateTracingSpan + validateTracer }; diff --git a/packages/data-point/src/DataPoint.test.js b/packages/data-point/src/DataPoint.test.js index eead88a7..99309a5a 100644 --- a/packages/data-point/src/DataPoint.test.js +++ b/packages/data-point/src/DataPoint.test.js @@ -6,10 +6,9 @@ const sayHello = value => `Hello ${value}`; const getAccumulator = (input, acc) => acc; const shallowTracer = { - // resolve will call this method, which expects a Span object in return - startSpan: () => shallowTracer, - setTag: () => true, - log: () => true + start: () => shallowTracer, + error: () => true, + finish: () => true }; describe("resolveFromAccumulator", () => { @@ -34,8 +33,7 @@ describe("resolveFromInput", () => { cache: { get: () => true, set: () => true - }, - tracer: "tracer" + } }; it("should pass locals object", async () => { const result = await dataPoint.resolveFromInput( @@ -113,40 +111,35 @@ describe("validateLocals", () => { }); }); -describe("validateTracingSpan", () => { +describe("validateTracer", () => { it("should only allow undefined or well defined tracer span API", () => { expect(() => { - dataPoint.validateTracingSpan(); + dataPoint.validateTracer(); }).not.toThrow(); expect(() => { - dataPoint.validateTracingSpan(shallowTracer); + dataPoint.validateTracer(shallowTracer); }).not.toThrow(); }); it("should throw error on any un-valid value", () => { expect(() => { - dataPoint.validateTracingSpan({}); - }).toThrowErrorMatchingInlineSnapshot( - `"tracer.startSpan must be a function, tracer expects opentracing API (see https://opentracing.io)"` - ); + dataPoint.validateTracer({}); + }).toThrowErrorMatchingInlineSnapshot(`"tracer.start must be a function"`); expect(() => { - dataPoint.validateTracingSpan({ - startSpan: () => true + dataPoint.validateTracer({ + start: () => true, + error: "invalid" }); - }).toThrowErrorMatchingInlineSnapshot( - `"tracer.setTag must be a function, tracer expects opentracing API (see https://opentracing.io)"` - ); + }).toThrowErrorMatchingInlineSnapshot(`"tracer.error must be a function"`); expect(() => { - dataPoint.validateTracingSpan({ - startSpan: () => true, - setTag: () => true + dataPoint.validateTracer({ + start: () => true, + finish: "invalid" }); - }).toThrowErrorMatchingInlineSnapshot( - `"tracer.log must be a function, tracer expects opentracing API (see https://opentracing.io)"` - ); + }).toThrowErrorMatchingInlineSnapshot(`"tracer.finish must be a function"`); }); }); diff --git a/packages/data-point/src/ReducerEntity.js b/packages/data-point/src/ReducerEntity.js index 6c9fdc19..3d3b7629 100644 --- a/packages/data-point/src/ReducerEntity.js +++ b/packages/data-point/src/ReducerEntity.js @@ -79,19 +79,15 @@ class ReducerEntity extends Reducer { try { acc.value = await this.resolveEntityValue(acc, resolveReducer); } catch (error) { - error.reducer = this; - - acc = acc.set("value", error); - - if (this.catch) { - acc = acc.set("value", await resolveReducer(acc, this.catch)); - if (this.outputType) { - await resolveReducer(acc, this.outputType); - } - return acc.value; + if (!this.catch) { + throw error; } - throw error; + acc = acc.set("value", await resolveReducer(acc, this.catch)); + if (this.outputType) { + await resolveReducer(acc, this.outputType); + } + return acc.value; } return acc.value; diff --git a/packages/data-point/src/ReducerEntity.test.js b/packages/data-point/src/ReducerEntity.test.js index 6020f30e..8f226178 100644 --- a/packages/data-point/src/ReducerEntity.test.js +++ b/packages/data-point/src/ReducerEntity.test.js @@ -139,7 +139,6 @@ describe("ReducerEntity", () => { } expect(error).toMatchInlineSnapshot(`[Error: entityError]`); - expect(error.reducer).toEqual(entity); }); it("should resolve this.catch", async () => { diff --git a/packages/data-point/src/resolve.js b/packages/data-point/src/resolve.js index f0c683a8..6e6fb70b 100644 --- a/packages/data-point/src/resolve.js +++ b/packages/data-point/src/resolve.js @@ -1,7 +1,8 @@ const uniqueIdScope = require("./unique-id-scope"); -const traceSpan = require("./trace-span"); +const traceSpan = require("./tracing/trace-span"); const pid = uniqueIdScope(); +const createThreadId = uniqueIdScope(); /** * Applies a Reducer to an accumulator @@ -10,12 +11,17 @@ const pid = uniqueIdScope(); * @param {Reducer} reducer * @returns {Promise} */ -async function resolve(accumulator, reducer) { +async function resolve(accumulator, reducer, newThread = false) { const acc = accumulator.create(); acc.reducer = reducer; + + if (newThread) { + acc.threadId = createThreadId(); + } + acc.pid = pid(); - const span = traceSpan.create(acc); + const span = traceSpan.start(acc, accumulator.reducer); let result; try { @@ -28,7 +34,13 @@ async function resolve(accumulator, reducer) { error.reducerStackTrace = acc.reducerStackTrace; } - traceSpan.logError(span, error); + // only set error.reducer once so any following catch does not override the + // original reducer. + if (!error.reducer) { + error.reducer = reducer; + } + + traceSpan.error(span, error); // rethrow the error up throw error; diff --git a/packages/data-point/src/resolve.test.js b/packages/data-point/src/resolve.test.js index 58698926..a52e5740 100644 --- a/packages/data-point/src/resolve.test.js +++ b/packages/data-point/src/resolve.test.js @@ -4,11 +4,11 @@ jest.mock("./unique-id-scope", () => { return () => mockPid; }); -jest.mock("./trace-span"); +jest.mock("./tracing/trace-span"); const { resolve } = require("./resolve"); const { Accumulator } = require("./Accumulator"); -const traceSpan = require("./trace-span"); +const traceSpan = require("./tracing/trace-span"); const reducer = { resolveReducer: jest.fn(() => "resolved") @@ -70,18 +70,53 @@ describe("resolve", () => { }); }); + describe("error.reducer", () => { + it("should augment error with reducer", async () => { + const acc = new Accumulator(); + + const testError = new Error("resolveReducer failed"); + + const reducerRejected = { + id: "badReducer", + resolveReducer: jest.fn().mockRejectedValue(testError) + }; + + const result = await resolve(acc, reducerRejected).catch(error => error); + + expect(result).toBeInstanceOf(Error); + expect(result.reducer).toEqual(reducerRejected); + }); + + it("should not augment error with reducer if already set", async () => { + const acc = new Accumulator(); + + const testError = new Error("resolveReducer failed"); + testError.reducer = "initialReducer"; + + const reducerRejected = { + id: "badReducer", + resolveReducer: jest.fn().mockRejectedValue(testError) + }; + + const result = await resolve(acc, reducerRejected).catch(error => error); + + expect(result).toBeInstanceOf(Error); + expect(result.reducer).toEqual("initialReducer"); + }); + }); + describe("tracing", () => { - it("should call traceSpan.create with accumulator", async () => { + it("should call traceSpan.start with accumulator", async () => { const acc = new Accumulator(); await resolve(acc, reducer); - const [accParam] = traceSpan.create.mock.calls[0]; + const [accParam] = traceSpan.start.mock.calls[0]; expect(accParam).toMatchObject({ pid: "pid", reducer }); }); - it("should call traceSpan.logError when resolveReducer throws error", async () => { + it("should call traceSpan.error when resolveReducer throws error", async () => { const acc = new Accumulator(); const testError = new Error("resolveReducer failed"); @@ -91,9 +126,9 @@ describe("resolve", () => { await resolve(acc, reducerRejected).catch(error => error); - expect(traceSpan.logError).toBeCalled(); + expect(traceSpan.error).toBeCalled(); - expect(traceSpan.logError.mock.calls[0]).toEqual([undefined, testError]); + expect(traceSpan.error.mock.calls[0]).toEqual([undefined, testError]); }); it("should finally call traceSpan.finish()", async () => { diff --git a/packages/data-point/src/trace-span.js b/packages/data-point/src/trace-span.js deleted file mode 100644 index 466c8c81..00000000 --- a/packages/data-point/src/trace-span.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * NOTE: mutates accumulator - * @private - * @param {*} accumulator - */ -function create(accumulator) { - if (!accumulator.tracer || !accumulator.tracer.startSpan) { - return undefined; - } - - const span = accumulator.tracer.startSpan(accumulator.reducer.id, { - childOf: accumulator.tracer - }); - - // passing reference to accumulator to create child-parent relationship - accumulator.tracer = span; - - span.setTag("pid", accumulator.pid); - - return span; -} - -function logError(span, error) { - if (!span || !span.log) { - return; - } - - span.log({ - event: "error", - "error.object": error, - message: error.message, - stack: error.stack - }); -} - -function finish(span) { - if (!span || !span.finish) { - return; - } - - span.finish(); -} - -module.exports = { - create, - logError, - finish -}; diff --git a/packages/data-point/src/trace-span.test.js b/packages/data-point/src/trace-span.test.js deleted file mode 100644 index 69ddd47e..00000000 --- a/packages/data-point/src/trace-span.test.js +++ /dev/null @@ -1,110 +0,0 @@ -const traceSpan = require("./trace-span"); -const { Accumulator } = require("./Accumulator"); - -describe("create", () => { - it("should return undefined if tracer is not defined", () => { - const acc = new Accumulator(); - expect(traceSpan.create(acc)).toEqual(undefined); - }); - - it("should return undefined if tracer.startSpan is not defined", () => { - const acc = new Accumulator(); - acc.tracer = {}; - expect(traceSpan.create(acc)).toEqual(undefined); - }); - - describe("create span when defined", () => { - // NOTE: grouping a couple of expect srtatements here to keep this tests - // simple - it("should create new span", () => { - const acc = new Accumulator(); - - acc.pid = "accPId"; - - acc.reducer = { - id: "reducerId" - }; - - const span = { - setTag: jest.fn() - }; - - const tracer = { - startSpan: jest.fn(() => span) - }; - - acc.tracer = tracer; - - // should return span - expect(traceSpan.create(acc)).toEqual(span); - // should set acc.tracer to new span - expect(acc.tracer).toEqual(span); - - // tracer.startSpan should follow opentracing API - expect(tracer.startSpan).toBeCalled(); - const [arg1, arg2] = tracer.startSpan.mock.calls[0]; - expect(arg1).toEqual("reducerId"); - // should set childOf relationship - expect(arg2).toMatchObject({ - childOf: tracer - }); - - expect(span.setTag).toBeCalledWith("pid", "accPId"); - }); - }); -}); - -describe("logError", () => { - it("should do nothing if span is not set", () => { - expect(() => { - traceSpan.logError(undefined); - }).not.toThrowError(); - }); - - it("should do nothing if span.log is not set", () => { - const span = {}; - expect(() => { - traceSpan.logError(span); - }).not.toThrowError(); - }); - - it("should call span.log with error information", () => { - const span = { - log: jest.fn() - }; - const error = new Error("test"); - traceSpan.logError(span, error); - expect(span.log).toHaveBeenCalled(); - const arg1 = span.log.mock.calls[0][0]; - expect(arg1).toMatchObject({ - event: "error", - "error.object": error, - message: "test", - stack: error.stack - }); - }); -}); - -describe("finish", () => { - it("should do nothing if span is not set", () => { - const span = undefined; - expect(() => { - traceSpan.finish(span); - }).not.toThrowError(); - }); - - it("should do nothing if span.finish is not set", () => { - const span = {}; - expect(() => { - traceSpan.finish(span); - }).not.toThrowError(); - }); - - it("should call span.finish with error information", () => { - const span = { - finish: jest.fn() - }; - traceSpan.finish(span); - expect(span.finish).toHaveBeenCalled(); - }); -}); diff --git a/packages/data-point/src/tracing/Span.js b/packages/data-point/src/tracing/Span.js new file mode 100644 index 00000000..d8db8939 --- /dev/null +++ b/packages/data-point/src/tracing/Span.js @@ -0,0 +1,51 @@ +function toNanoSeconds(hrtime) { + return hrtime[0] * 1e9 + hrtime[1]; +} + +function getHighResolutionTime() { + return toNanoSeconds(process.hrtime()); +} + +/** + * @alias Span + */ +class Span { + constructor(name, options, tracer) { + this.root = false; + this.name = name; + this.parent = options.parent; + this.context = options.context; + this.tracer = tracer; + this.timeStartNs = getHighResolutionTime(); + this.timeEndNs = undefined; + this.errors = undefined; + this.data = {}; + } + + start(name, options) { + const span = new Span(name, options, this.tracer); + this.tracer.handlers.start(span); + return span; + } + + setError(err) { + this.error = err; + if (typeof this.tracer.handlers.error === "function") { + this.tracer.handlers.error(this, err); + } + } + + finish() { + this.timeEndNs = getHighResolutionTime(); + + if (typeof this.tracer.handlers.finish === "function") { + this.tracer.handlers.finish(this); + } + } +} + +module.exports = { + toNanoSeconds, + getHighResolutionTime, + Span +}; diff --git a/packages/data-point/src/tracing/Span.test.js b/packages/data-point/src/tracing/Span.test.js new file mode 100644 index 00000000..5b7a2e9e --- /dev/null +++ b/packages/data-point/src/tracing/Span.test.js @@ -0,0 +1,121 @@ +const { toNanoSeconds, getHighResolutionTime, Span } = require("./Span"); + +const handlers = { + start: jest.fn() +}; + +const tracer = { + handlers +}; + +afterEach(() => { + jest.clearAllMocks(); +}); + +const mockHrtimeResult = [75238, 684529285]; +const mockHighResolutionTime = 75238684529285; + +describe("toNanoSeconds", () => { + it("should convert hrtime to nanoseconds", () => { + expect(toNanoSeconds(mockHrtimeResult)).toEqual(mockHighResolutionTime); + }); +}); + +describe("getHighResolutionTime", () => { + it("should get nanoseconds from current hrtime", () => { + const spyProcessHrtime = jest + .spyOn(process, "hrtime") + .mockReturnValue(mockHrtimeResult); + + expect(getHighResolutionTime()).toEqual(mockHighResolutionTime); + + spyProcessHrtime.mockRestore(); + }); +}); + +describe("Span", () => { + describe("constructor", () => { + it("should set name", () => { + expect(new Span("name", {}, tracer)).toHaveProperty("name", "name"); + }); + + it("should set tracer", () => { + expect(new Span("name", {}, tracer)).toHaveProperty("tracer", tracer); + }); + + it("should set timestamps", () => { + const spyProcessHrtime = jest + .spyOn(process, "hrtime") + .mockReturnValue(mockHrtimeResult); + + const span = new Span("name", {}, tracer); + expect(span).toHaveProperty("timeStartNs", mockHighResolutionTime); + expect(span).toHaveProperty("timeEndNs", undefined); + + spyProcessHrtime.mockRestore(); + }); + }); + + describe("start", () => { + it("should create a span and call handlers.start", () => { + const span = new Span("name", {}, tracer); + const newSpan = span.start("newSpan", { + context: {}, + parent: {} + }); + expect(newSpan).toBeInstanceOf(Span); + expect(newSpan).toHaveProperty("name", "newSpan"); + expect(newSpan).toHaveProperty("tracer", tracer); + expect(handlers.start).toHaveBeenCalledWith(newSpan); + }); + }); + + describe("setError", () => { + it("should set error", () => { + const span = new Span("name", {}, tracer); + const error = new Error("test"); + span.setError(error); + expect(span).toHaveProperty("error", error); + }); + + it("should call tracer.handlers.error if available", () => { + const tracerWithError = { + handlers: { + start: jest.fn(), + error: jest.fn() + } + }; + + const span = new Span("name", {}, tracerWithError); + const error = new Error("test"); + span.setError(error); + expect(tracerWithError.handlers.error).toHaveBeenCalledWith(span, error); + }); + }); + + describe("finish", () => { + it("should set timeEndNs", () => { + const span = new Span("name", {}, tracer); + const spyProcessHrtime = jest + .spyOn(process, "hrtime") + .mockReturnValue(mockHrtimeResult); + + span.finish(); + expect(span).toHaveProperty("timeEndNs", mockHighResolutionTime); + spyProcessHrtime.mockRestore(); + }); + + it("should call tracer.handlers.finish if available", () => { + const tracerWithFinish = { + handlers: { + start: jest.fn(), + finish: jest.fn() + } + }; + + const span = new Span("name", {}, tracerWithFinish); + span.finish(); + expect(tracerWithFinish.handlers.finish).toHaveBeenCalledWith(span); + }); + }); +}); diff --git a/packages/data-point/src/tracing/Tracer.js b/packages/data-point/src/tracing/Tracer.js new file mode 100644 index 00000000..2418c7b3 --- /dev/null +++ b/packages/data-point/src/tracing/Tracer.js @@ -0,0 +1,17 @@ +const { Span } = require("./Span"); + +class Tracer { + constructor(handlers) { + this.handlers = handlers; + } + + start(name, options) { + const span = new Span(name, options, this); + span.root = true; + + this.handlers.start(span); + return span; + } +} + +module.exports = Tracer; diff --git a/packages/data-point/src/tracing/Tracer.test.js b/packages/data-point/src/tracing/Tracer.test.js new file mode 100644 index 00000000..76772e0e --- /dev/null +++ b/packages/data-point/src/tracing/Tracer.test.js @@ -0,0 +1,31 @@ +const Tracer = require("./Tracer"); + +const handlers = { + start: jest.fn() +}; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe("Tracer", () => { + describe("constructor", () => { + it("should set handlers", () => { + expect(new Tracer(handlers)).toHaveProperty("handlers", handlers); + }); + }); + + describe("start", () => { + it("should create root span", () => { + const tracer = new Tracer(handlers); + const span = tracer.start("name", {}); + expect(span).toHaveProperty("root", true); + }); + + it("should call handlers.start", () => { + const tracer = new Tracer(handlers); + const span = tracer.start("name", {}); + expect(handlers.start).toBeCalledWith(span); + }); + }); +}); diff --git a/packages/data-point/src/tracing/trace-span.js b/packages/data-point/src/tracing/trace-span.js new file mode 100644 index 00000000..a97d72c2 --- /dev/null +++ b/packages/data-point/src/tracing/trace-span.js @@ -0,0 +1,50 @@ +/** + * NOTE: mutates accumulator + * @private + * @param {*} accumulator + */ +function start(accumulator) { + if (!accumulator.tracer) { + return undefined; + } + + const span = accumulator.tracer.start(accumulator.reducer.id, { + context: accumulator, + parent: accumulator.tracer + }); + + // passing reference to accumulator to create child-parent relationship + accumulator.tracer = span; + + return span; +} +/** + * @private + * @param {Span} span + * @param {Error} spanError + */ +function error(span, spanError) { + if (!span) { + return; + } + + span.setError(spanError); +} + +/** + * @private + * @param {Span} span + */ +function finish(span) { + if (!span) { + return; + } + + span.finish(); +} + +module.exports = { + start, + error, + finish +}; diff --git a/packages/data-point/src/tracing/trace-span.test.js b/packages/data-point/src/tracing/trace-span.test.js new file mode 100644 index 00000000..aad833ee --- /dev/null +++ b/packages/data-point/src/tracing/trace-span.test.js @@ -0,0 +1,77 @@ +const traceSpan = require("./trace-span"); +const { Accumulator } = require("../Accumulator"); + +describe("start", () => { + it("should return undefined if tracer is not defined", () => { + const acc = new Accumulator(); + expect(traceSpan.start(acc)).toEqual(undefined); + }); + + describe("start span when defined", () => { + // NOTE: grouping a couple of expect statements here to keep this tests + // simple + it("should start new span", () => { + const acc = new Accumulator(); + + acc.pid = "accPId"; + + acc.reducer = { + id: "reducerId" + }; + + const span = { + setTag: jest.fn() + }; + + const tracer = { + start: jest.fn(() => span) + }; + + acc.tracer = tracer; + + // should return span + expect(traceSpan.start(acc)).toEqual(span); + // should set acc.tracer to new span + expect(acc.tracer).toEqual(span); + + expect(tracer.start).toBeCalledWith("reducerId", { + context: acc, + parent: tracer + }); + }); + }); +}); + +describe("error", () => { + it("should do nothing if span is not set", () => { + expect(() => { + traceSpan.error(undefined); + }).not.toThrowError(); + }); + + it("should call span.setError with error object", () => { + const span = { + setError: jest.fn() + }; + const error = new Error("test"); + traceSpan.error(span, error); + expect(span.setError).toHaveBeenCalledWith(error); + }); +}); + +describe("finish", () => { + it("should do nothing if span is not set", () => { + const span = undefined; + expect(() => { + traceSpan.finish(span); + }).not.toThrowError(); + }); + + it("should call span.finish with error information", () => { + const span = { + finish: jest.fn() + }; + traceSpan.finish(span); + expect(span.finish).toHaveBeenCalled(); + }); +});