From 8542c94b2ebfeb12a49ab06d2cccbc609ae58a90 Mon Sep 17 00:00:00 2001 From: vigneshshanmugam Date: Mon, 12 Sep 2022 12:01:20 -0700 Subject: [PATCH 1/3] feat: support cross-linking synthetics & apm --- src/common_types.ts | 6 ++++ src/core/gatherer.ts | 3 +- src/core/runner.ts | 2 ++ src/options.ts | 2 ++ src/plugins/apm.ts | 58 +++++++++++++++++++++++++++++++++++ src/plugins/index.ts | 1 + src/plugins/plugin-manager.ts | 25 ++++++++++++--- 7 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 src/plugins/apm.ts diff --git a/src/common_types.ts b/src/common_types.ts index 876f3fab..a94c2354 100644 --- a/src/common_types.ts +++ b/src/common_types.ts @@ -239,6 +239,7 @@ export type RunOptions = BaseArgs & { playwrightOptions?: PlaywrightOptions; networkConditions?: NetworkConditions; reporter?: BuiltInReporterName | ReporterInstance; + apm?: ApmOptions; }; export type PushOptions = ProjectSettings & { @@ -254,12 +255,17 @@ export type ProjectSettings = { space: string; }; +export type ApmOptions = { + origins: Array; +}; + export type PlaywrightOptions = LaunchOptions & BrowserContextOptions; export type SyntheticsConfig = { params?: Params; playwrightOptions?: PlaywrightOptions; monitor?: MonitorConfig; project?: ProjectSettings; + apm?: ApmOptions; }; /** Runner Payload types */ diff --git a/src/core/gatherer.ts b/src/core/gatherer.ts index 611a7dcf..b0a4f673 100644 --- a/src/core/gatherer.ts +++ b/src/core/gatherer.ts @@ -109,12 +109,13 @@ export class Gatherer { */ static async beginRecording(driver: Driver, options: RunOptions) { log('Gatherer: started recording'); - const { network, metrics } = options; + const { network, metrics, apm } = options; const pluginManager = new PluginManager(driver); pluginManager.registerAll(options); const plugins = [await pluginManager.start('browserconsole')]; network && plugins.push(await pluginManager.start('network')); metrics && plugins.push(await pluginManager.start('performance')); + apm && plugins.push(await pluginManager.start('apm')); await Promise.all(plugins); return pluginManager; } diff --git a/src/core/runner.ts b/src/core/runner.ts index 66aa6eb0..ab33d4b5 100644 --- a/src/core/runner.ts +++ b/src/core/runner.ts @@ -23,6 +23,7 @@ * */ +import { randomBytes } from 'crypto'; import { join } from 'path'; import { mkdir, rm, writeFile } from 'fs/promises'; import { Journey } from '../dsl/journey'; @@ -50,6 +51,7 @@ import { PerformanceManager } from '../plugins'; import { Gatherer } from './gatherer'; import { log } from './logger'; import { Monitor, MonitorConfig } from '../dsl/monitor'; +import { Request, Route } from 'playwright-core'; type HookType = 'beforeAll' | 'afterAll'; export type SuiteHooks = Record>; diff --git a/src/options.ts b/src/options.ts index 0ba2cef6..ee2652ac 100644 --- a/src/options.ts +++ b/src/options.ts @@ -140,6 +140,8 @@ export function normalizeOptions(cliArgs: CliArgs): RunOptions { options.privateLocations = cliArgs.privateLocations ?? monitor?.privateLocations; + options.apm = config.apm; + return options; } diff --git a/src/plugins/apm.ts b/src/plugins/apm.ts new file mode 100644 index 00000000..5998f710 --- /dev/null +++ b/src/plugins/apm.ts @@ -0,0 +1,58 @@ +/** + * MIT License + * + * Copyright (c) 2020-present, Elastic NV + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +import { randomBytes } from 'crypto'; +import { Request, Route } from 'playwright-core'; +import { ApmOptions, Driver } from '../common_types'; + +export class Apm { + constructor(private driver: Driver, private options: ApmOptions) {} + + async traceHandler(route: Route, request: Request) { + const traceId = randomBytes(16).toString('hex'); + // We dont want to track Synthetics, so sending a dummy id + const parentId = '0'.repeat(16); + const traceparent = `00-${traceId}-${parentId}-01`; + const headers = { + ...(await request.allHeaders()), + traceparent, + }; + route.continue({ + headers, + }); + } + + async start() { + for (const origin of this.options.origins) { + await this.driver.context.route(origin, this.traceHandler.bind(this)); + } + } + + async stop() { + for (const origin of this.options.origins) { + await this.driver.context.unroute(origin, this.traceHandler.bind(this)); + } + } +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 9de1b9b6..3725f3ad 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -28,3 +28,4 @@ export * from './network'; export * from './performance'; export * from './tracing'; export * from './browser-console'; +export * from './apm'; diff --git a/src/plugins/plugin-manager.ts b/src/plugins/plugin-manager.ts index 740bcb0b..69f64bb7 100644 --- a/src/plugins/plugin-manager.ts +++ b/src/plugins/plugin-manager.ts @@ -23,8 +23,9 @@ * */ -import { PluginOutput, Driver } from '../common_types'; +import { PluginOutput, Driver, ApmOptions } from '../common_types'; import { + Apm, BrowserConsole, NetworkManager, PerformanceManager, @@ -33,9 +34,21 @@ import { } from './'; import { Step } from '../dsl'; -type PluginType = 'network' | 'trace' | 'performance' | 'browserconsole'; -type Plugin = NetworkManager | Tracing | PerformanceManager | BrowserConsole; -type PluginOptions = TraceOptions; +type PluginType = + | 'network' + | 'trace' + | 'performance' + | 'browserconsole' + | 'apm'; +type Plugin = + | NetworkManager + | Tracing + | PerformanceManager + | BrowserConsole + | Apm; +type PluginOptions = TraceOptions & { + apm?: ApmOptions; +}; export class PluginManager { protected plugins = new Map(); @@ -44,6 +57,7 @@ export class PluginManager { 'trace', 'performance', 'browserconsole', + 'apm', ]; constructor(private driver: Driver) {} @@ -62,6 +76,9 @@ export class PluginManager { case 'browserconsole': instance = new BrowserConsole(this.driver); break; + case 'apm': + instance = new Apm(this.driver, options.apm); + break; } instance && this.plugins.set(type, instance); return instance; From 2fad9b7761f2f5f4d302cb4e6ae118ddb3d06b5f Mon Sep 17 00:00:00 2001 From: vigneshshanmugam Date: Fri, 4 Aug 2023 16:30:39 -0700 Subject: [PATCH 2/3] add baggage and tracestate --- __tests__/plugins/apm.test.ts | 63 +++++++++++++++++++++++ src/common_types.ts | 3 +- src/core/runner.ts | 2 - src/options.ts | 2 +- src/plugins/apm.ts | 97 ++++++++++++++++++++++++++++++----- 5 files changed, 149 insertions(+), 18 deletions(-) create mode 100644 __tests__/plugins/apm.test.ts diff --git a/__tests__/plugins/apm.test.ts b/__tests__/plugins/apm.test.ts new file mode 100644 index 00000000..9e4b1059 --- /dev/null +++ b/__tests__/plugins/apm.test.ts @@ -0,0 +1,63 @@ +/** + * MIT License + * + * Copyright (c) 2020-present, Elastic NV + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +import { journey, runner } from '../../src/core'; +import { Gatherer } from '../../src/core/gatherer'; +import { NetworkManager } from '../../src/plugins'; +import { Apm, BAGGAGE_HEADER, TRACE_STATE_HEADER } from '../../src/plugins/apm'; +import { Server } from '../utils/server'; +import { wsEndpoint } from '../utils/test-config'; + +describe('apm', () => { + let server: Server; + beforeAll(async () => { + server = await Server.create(); + }); + afterAll(async () => { + await server.close(); + }); + + it('add baggage header', async () => { + const driver = await Gatherer.setupDriver({ + wsEndpoint, + }); + // test journey + runner.addJourney(journey('example-journey', () => {})); + const network = new NetworkManager(driver); + const apm = new Apm(driver, { traceUrls: ['**/*'] }); + await network.start(); + await apm.start(); + + await driver.page.goto(server.TEST_PAGE); + await apm.stop(); + const [htmlReq] = await network.stop(); + await Gatherer.stop(); + + expect(htmlReq.request.headers[BAGGAGE_HEADER]).toBe( + `synthetics.monitor.id=example-journey;` + ); + expect(htmlReq.request.headers[TRACE_STATE_HEADER]).toBe(`es=s:1`); + }); +}); diff --git a/src/common_types.ts b/src/common_types.ts index e06e6335..072347b8 100644 --- a/src/common_types.ts +++ b/src/common_types.ts @@ -254,7 +254,8 @@ export type ProjectSettings = { }; export type ApmOptions = { - origins: Array; + traceUrls: Array; + sampleRate?: number; }; export type PlaywrightOptions = LaunchOptions & diff --git a/src/core/runner.ts b/src/core/runner.ts index 1d0b821d..6949a66a 100644 --- a/src/core/runner.ts +++ b/src/core/runner.ts @@ -23,7 +23,6 @@ * */ -import { randomBytes } from 'crypto'; import { join } from 'path'; import { mkdir, rm, writeFile } from 'fs/promises'; import { Journey } from '../dsl/journey'; @@ -52,7 +51,6 @@ import { PerformanceManager } from '../plugins'; import { Gatherer } from './gatherer'; import { log } from './logger'; import { Monitor, MonitorConfig } from '../dsl/monitor'; -import { Request, Route } from 'playwright-core'; type HookType = 'beforeAll' | 'afterAll'; export type SuiteHooks = Record>; diff --git a/src/options.ts b/src/options.ts index af5c1681..88a122c3 100644 --- a/src/options.ts +++ b/src/options.ts @@ -137,7 +137,7 @@ export function normalizeOptions( } break; } - + // Enables the tracing on the journey level options.apm = config.apm; return options; diff --git a/src/plugins/apm.ts b/src/plugins/apm.ts index 5998f710..24c6d15f 100644 --- a/src/plugins/apm.ts +++ b/src/plugins/apm.ts @@ -23,36 +23,105 @@ * */ -import { randomBytes } from 'crypto'; import { Request, Route } from 'playwright-core'; import { ApmOptions, Driver } from '../common_types'; +import { runner } from '../core'; +import { Journey } from '../dsl'; + +/** + * Baggage header is used to propagate user defined properties across distributed systems. + * https://www.w3.org/TR/baggage/ + */ +export const BAGGAGE_HEADER = 'baggage'; +/** + * Tracestate header is used to provide vendor specific trace identification. + * https://www.w3.org/TR/trace-context/#tracestate-header + */ +export const TRACE_STATE_HEADER = 'tracestate'; +const NAMESPACE = 'es'; +const PROPERTY_SEPARATOR = '='; + +function getValueFromEnv(key: string) { + return process.env?.[key]; +} + +/** + * Generate the tracestate header in the elastic namespace which is understood by + * all of our apm agents + * https://github.com/elastic/apm/blob/main/specs/agents/tracing-distributed-tracing.md#tracestate + * + * rate must be in the range [0,1] rounded up to 4 decimal precision (0.0001, 0.8122, ) + */ +function genTraceStateHeader(rate = 1) { + if (isNaN(rate) || rate < 0 || rate > 1) { + rate = 1; + } else if (rate > 0 && rate < 0.001) { + rate = 0.001; + } else { + rate = Math.round(rate * 10000) / 10000; + } + return `${NAMESPACE}${PROPERTY_SEPARATOR}s:${rate}`; +} + +/** + * Generate the baggage header to be propagated to the destination routes + * + * We are interested in the following properties + * 1. Monitor ID - monitor id of the synthetics monitor + * 2. Trace id - checkgroup/exec that begins the synthetics journey + * 3. Location - location where the synthetics monitor is run from + * 4. Type - type of the synthetics monitor (browser, http, tcp, etc) + */ +function generateBaggageHeader(journey: Journey) { + let monitorId = getValueFromEnv('ELASTIC_SYNTHETICS_MONITOR_ID'); + if (!monitorId) { + monitorId = journey?.monitor.config.id; + } + + const baggageObj = { + 'synthetics.trace.id': getValueFromEnv('ELASTIC_SYNTHETICS_TRACE_ID'), + 'synthetics.monitor.id': monitorId, + 'synthetics.monitor.type': getValueFromEnv( + 'ELASTIC_SYNTHETICS_MONITOR_TYPE' + ), + 'synthetics.monitor.location': getValueFromEnv( + 'ELASTIC_SYNTHETICS_MONITOR_LOCATION' + ), + }; + + let baggage = ''; + for (const key of Object.keys(baggageObj)) { + if (baggageObj[key]) { + baggage += `${key}${PROPERTY_SEPARATOR}${baggageObj[key]};`; + } + } + + return baggage; +} export class Apm { constructor(private driver: Driver, private options: ApmOptions) {} async traceHandler(route: Route, request: Request) { - const traceId = randomBytes(16).toString('hex'); - // We dont want to track Synthetics, so sending a dummy id - const parentId = '0'.repeat(16); - const traceparent = `00-${traceId}-${parentId}-01`; - const headers = { - ...(await request.allHeaders()), - traceparent, - }; + // Propagate baggae headers to the urls route.continue({ - headers, + headers: { + ...request.headers(), + [BAGGAGE_HEADER]: generateBaggageHeader(runner.currentJourney), + [TRACE_STATE_HEADER]: genTraceStateHeader(this.options.sampleRate), + }, }); } async start() { - for (const origin of this.options.origins) { - await this.driver.context.route(origin, this.traceHandler.bind(this)); + for (const url of this.options.traceUrls) { + await this.driver.context.route(url, this.traceHandler.bind(this)); } } async stop() { - for (const origin of this.options.origins) { - await this.driver.context.unroute(origin, this.traceHandler.bind(this)); + for (const url of this.options.traceUrls) { + await this.driver.context.unroute(url, this.traceHandler.bind(this)); } } } From 8c6e61ad3a50ba3306a50aa00684b95740d68a65 Mon Sep 17 00:00:00 2001 From: vigneshshanmugam Date: Fri, 4 Aug 2023 17:05:53 -0700 Subject: [PATCH 3/3] more tests --- __tests__/plugins/apm.test.ts | 47 +++++++++++++++++++++++++++++------ src/plugins/apm.ts | 4 +-- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/__tests__/plugins/apm.test.ts b/__tests__/plugins/apm.test.ts index 9e4b1059..d6ed23de 100644 --- a/__tests__/plugins/apm.test.ts +++ b/__tests__/plugins/apm.test.ts @@ -23,10 +23,16 @@ * */ -import { journey, runner } from '../../src/core'; +import { journey, runner, monitor } from '../../src/core'; import { Gatherer } from '../../src/core/gatherer'; import { NetworkManager } from '../../src/plugins'; -import { Apm, BAGGAGE_HEADER, TRACE_STATE_HEADER } from '../../src/plugins/apm'; +import { + Apm, + BAGGAGE_HEADER, + TRACE_STATE_HEADER, + genTraceStateHeader, + generateBaggageHeader, +} from '../../src/plugins/apm'; import { Server } from '../utils/server'; import { wsEndpoint } from '../utils/test-config'; @@ -39,25 +45,52 @@ describe('apm', () => { await server.close(); }); - it('add baggage header', async () => { + it('propagate http header', async () => { + runner.registerJourney( + journey('j1', () => {}), + {} as any + ); const driver = await Gatherer.setupDriver({ wsEndpoint, }); - // test journey - runner.addJourney(journey('example-journey', () => {})); const network = new NetworkManager(driver); const apm = new Apm(driver, { traceUrls: ['**/*'] }); await network.start(); await apm.start(); - + // visit test page await driver.page.goto(server.TEST_PAGE); await apm.stop(); const [htmlReq] = await network.stop(); await Gatherer.stop(); expect(htmlReq.request.headers[BAGGAGE_HEADER]).toBe( - `synthetics.monitor.id=example-journey;` + `synthetics.monitor.id=j1;` ); expect(htmlReq.request.headers[TRACE_STATE_HEADER]).toBe(`es=s:1`); }); + + it('baggage generation', () => { + const j1 = journey('j1', () => { + monitor.use({ id: 'foo' }); + }); + runner.registerJourney(j1, {} as any); + expect(generateBaggageHeader(j1)).toBe(`synthetics.monitor.id=foo;`); + + // Set Checkgroup + process.env['ELASTIC_SYNTHETICS_TRACE_ID'] = 'x-trace'; + process.env['ELASTIC_SYNTHETICS_MONITOR_ID'] = 'global-foo'; + expect(generateBaggageHeader(j1)).toBe( + `synthetics.trace.id=x-trace;synthetics.monitor.id=global-foo;` + ); + + delete process.env['ELASTIC_SYNTHETICS_TRACE_ID']; + delete process.env['ELASTIC_SYNTHETICS_MONITOR_ID']; + }); + + it('tracestate generation', () => { + expect(genTraceStateHeader(0.5)).toBe(`es=s:0.5`); + expect(genTraceStateHeader(0.921132)).toBe(`es=s:0.9211`); + expect(genTraceStateHeader(-1)).toBe(`es=s:1`); + expect(genTraceStateHeader(20)).toBe(`es=s:1`); + }); }); diff --git a/src/plugins/apm.ts b/src/plugins/apm.ts index 24c6d15f..1c74bf50 100644 --- a/src/plugins/apm.ts +++ b/src/plugins/apm.ts @@ -52,7 +52,7 @@ function getValueFromEnv(key: string) { * * rate must be in the range [0,1] rounded up to 4 decimal precision (0.0001, 0.8122, ) */ -function genTraceStateHeader(rate = 1) { +export function genTraceStateHeader(rate = 1) { if (isNaN(rate) || rate < 0 || rate > 1) { rate = 1; } else if (rate > 0 && rate < 0.001) { @@ -72,7 +72,7 @@ function genTraceStateHeader(rate = 1) { * 3. Location - location where the synthetics monitor is run from * 4. Type - type of the synthetics monitor (browser, http, tcp, etc) */ -function generateBaggageHeader(journey: Journey) { +export function generateBaggageHeader(journey: Journey) { let monitorId = getValueFromEnv('ELASTIC_SYNTHETICS_MONITOR_ID'); if (!monitorId) { monitorId = journey?.monitor.config.id;