Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[scout] improve logs readability by using unique logger context per playwright worker #213218

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import { ToolingLog } from '@kbn/tooling-log';

export class ScoutLogger extends ToolingLog {
constructor() {
super({ level: 'verbose', writeTo: process.stdout }, { context: 'scout' });
constructor(workerContext: string) {
super({ level: 'verbose', writeTo: process.stdout }, { context: workerContext });
this.serviceLoaded('logger');
}

Expand All @@ -20,20 +20,28 @@ export class ScoutLogger extends ToolingLog {
* @param name unique name of the service
*/
public serviceLoaded(name: string) {
this.debug(`[service] ${name}`);
this.debug(`[${name}] loaded`);
}

/**
* Used to log a message for a service/fixture
*/
public serviceMessage(name: string, message: string) {
this.debug(`[${name}] ${name}`);
}
}

let loggerInstance: ScoutLogger | null = null;
const loggerInstances = new Map<string, ScoutLogger>();

/**
* Singleton logger instance to share across the Scout components
* @returns {ScoutLogger}
* Singleton logger instance for specific worker to share across the Scout components
* @param workerContext logger context, e.g. `scout-worker-1`; default is `scout`
* @returns {ScoutLogger} logger instance
*/
export function getLogger(): ScoutLogger {
if (!loggerInstance) {
loggerInstance = new ScoutLogger();
export function getLogger(workerContext: string = 'scout'): ScoutLogger {
if (!loggerInstances.has(workerContext)) {
loggerInstances.set(workerContext, new ScoutLogger(workerContext));
}

return loggerInstance;
return loggerInstances.get(workerContext)!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import {
validateTagsFixture,
} from './test';
import type { BrowserAuthFixture, ScoutPage, PageObjects } from './test';
import { logParallelFixture } from './worker/log';

export const scoutParallelFixtures = mergeTests(
// worker scope fixtures
logParallelFixture,
coreWorkerFixtures,
scoutSpaceParallelFixture,
apiFixtures,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ import {
validateTagsFixture,
} from './test';
import type { BrowserAuthFixture, ScoutPage, PageObjects } from './test';
import { logFixture } from './worker/log';
export type { ScoutPage, PageObjects } from './test';

export const scoutFixtures = mergeTests(
// worker scope fixtures
logFixture,
coreWorkerFixtures,
esArchiverFixture,
uiSettingsFixture,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export const pageObjectsParallelFixture = scoutPageParallelFixture.extend<
},
{ scoutSpace: ScoutSpaceParallelFixture }
>({
pageObjects: async ({ page, log, scoutSpace }, use) => {
pageObjects: async ({ page, log }, use) => {
const corePageObjects = createCorePageObjects(page);
log.serviceLoaded(`pageObjects:${scoutSpace.id}`);
Copy link
Member Author

Choose a reason for hiding this comment

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

I believe it is redundant: logger context helps to understand the space we use, there is a 1 to 1 match.

Copy link

@csr csr Mar 5, 2025

Choose a reason for hiding this comment

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

Sounds good about the test space ID being redundant.

A possible improvement for the future (possibly out of scope for this PR): instead of logging just pageObjects (the service name), we could log pageObjects loaded or perhaps pageObjects service loaded to make it clear the service was loaded (successfully). What do you think?

Copy link
Member Author

@dmlemeshko dmlemeshko Mar 5, 2025

Choose a reason for hiding this comment

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

I was thinking about it too, how about having it this way (actually it was in PoC like that):

  /**
   * Used to log when a service/fixture is loaded
   * @param name unique name of the service
   */
  public serviceLoaded(name: string) {
    this.debug(`[${name}] loaded`);
  }

  /**
   * Used to log a message for a service/fixture
   */
  public serviceMessage(name: string, message) {
    this.debug(`[${name}] ${name}`);
  }

output

debg [scout-worker-2] [scoutPage] loaded
debg [scout-worker-2] [pageObjects] loaded
debg [scout-worker-2] [scoutPage] just a random message from service

alternatively

debg [scout-worker-2] [service] 'scoutPage' loaded
debg [scout-worker-2] [service] 'pageObjects' loaded
debg [scout-worker-2]  [service] [pageObjects] just a random message from service

but maybe we don't need that [service] context assuming we know that it is coming from playwright worker?

Copy link
Member Author

Choose a reason for hiding this comment

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

locally run

[chromium] › saved_searches.spec.ts:66:14 › Discover app - saved searches › should customize time range on dashboards @ess @svlSearch @svlOblt
 debg [scout-worker-2] [logger] loaded
 info [scout-worker-2] [config] Reading test servers confiuration from file: /Users/dmle/github/kibana/.scout/servers/local.json
 debg [scout-worker-2] [config] loaded
 debg [scout-worker-2] [kbnClient] loaded
 debg [scout-worker-2] Requesting url (redacted): [http://localhost:5620/api/spaces/space]
[chromium] › error_handling.spec.ts:30:12 › Discover app - errors › should render invalid scripted field error @ess
 debg [scout-worker-1] [logger] loaded
 info [scout-worker-1] [config] Reading test servers confiuration from file: /Users/dmle/github/kibana/.scout/servers/local.json
 debg [scout-worker-1] [config] loaded
 debg [scout-worker-1] [kbnClient] loaded
 debg [scout-worker-1] Requesting url (redacted): [http://localhost:5620/api/spaces/space]

Copy link

Choose a reason for hiding this comment

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

I like the [service] extra bit of information, I think it adds clarity to the log, and it doesn't take a lot of space after all, but I'm fine with leaving it out (up to you).

log.serviceLoaded(`pageObjects`);
await use(corePageObjects);
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const scoutPageParallelFixture = base.extend<
extendedPage.gotoApp = (appName: string, pathOptions?: PathOptions) =>
page.goto(kbnUrl.app(appName, { space: scoutSpace.id, pathOptions }));

log.serviceLoaded(`scoutPage:${scoutSpace.id}`);
Copy link
Member Author

Choose a reason for hiding this comment

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

same here, scout-worker-N logger context is good enough

log.serviceLoaded(`scoutPage`);
await use(extendedPage);
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,16 @@ import {
createKbnUrl,
getEsClient,
getKbnClient,
getLogger,
createSamlSessionManager,
createScoutConfig,
KibanaUrl,
} from '../../../common/services';
import { ScoutTestOptions } from '../../types';
import { ScoutTestConfig } from '.';
import { ScoutLogger } from '../../../common';
import type { ScoutTestOptions } from '../../types';
import type { ScoutTestConfig } from '.';
import type { ScoutLogger } from './log';

// re-export to import types from '@kbn-scout'
export type { KbnClient, SamlSessionManager } from '@kbn/test';
export type { ScoutLogger } from '../../../common';
export type { Client as EsClient } from '@elastic/elasticsearch';
export type { KibanaUrl } from '../../../common/services/kibana_url';
export type { ScoutTestConfig } from '../../../types';
Expand All @@ -48,15 +46,6 @@ export const coreWorkerFixtures = base.extend<
samlAuth: SamlSessionManager;
}
>({
// Provides a scoped logger instance for each worker. This logger is shared across
// all other fixtures within the worker scope.
log: [
({}, use) => {
use(getLogger());
},
{ scope: 'worker' },
],

/**
* Loads the test server configuration from the source file based on local or cloud
* target, located by default in '.scout/servers' directory. It supplies Playwright
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

export { coreWorkerFixtures } from './core_fixtures';
export type {
ScoutLogger,
ScoutTestConfig,
KibanaUrl,
EsClient,
Expand All @@ -31,3 +30,6 @@ export type { ApiFixtures, ApiParallelWorkerFixtures } from './apis';

export { synthtraceFixture } from './synthtrace';
export type { SynthtraceFixture } from './synthtrace';

export { logFixture, logParallelFixture } from './log';
export type { ScoutLogger } from './log';
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { logFixture } from './single_thread';
export { logParallelFixture } from './parallel';
export type { ScoutLogger } from '../../../../common';
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { test as base } from '@playwright/test';
import { getLogger } from '../../../../common/services';
import { ScoutLogger } from '../../../../common';

/**
* Provides a scoped logger instance for each worker. This logger is shared across
* all other fixtures within the worker scope. This fixture is used in the parallel
* test execution mode to better diffirentiate between logs from different workers: `scout-1`, `scout-2`, etc.
*/
export const logParallelFixture = base.extend<
{},
{
log: ScoutLogger;
}
>({
log: [
({}, use, workerInfo) => {
const loggerContext = `scout-worker-${workerInfo.parallelIndex + 1}`;
use(getLogger(loggerContext));
},
{ scope: 'worker' },
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { test as base } from '@playwright/test';
import { getLogger } from '../../../../common/services';
import { ScoutLogger } from '../../../../common';

/**
* Provides a scoped logger instance for each worker. This logger is shared across
* all other fixtures within the worker scope.
*/
export const logFixture = base.extend<
{},
{
log: ScoutLogger;
}
>({
log: [
({}, use) => {
use(getLogger('scout-worker'));
},
{ scope: 'worker' },
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,42 @@ export const scoutSpaceParallelFixture = coreWorkerFixtures.extend<
>({
scoutSpace: [
async ({ log, kbnClient }, use, workerInfo) => {
const spaceId = `test-space-${workerInfo.workerIndex}`;
const spaceId = `test-space-${workerInfo.parallelIndex + 1}`;
Copy link
Member Author

@dmlemeshko dmlemeshko Mar 5, 2025

Choose a reason for hiding this comment

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

workerIndex vs parallelIndex:

Playwright created new worker after failure, workerIndex will be increased. Since we delete Kibana space on fixture tear down, we can and should use persistent index - parallelIndex

https://playwright.dev/docs/api/class-workerinfo#worker-info-parallel-index

const spacePayload = {
id: spaceId,
name: spaceId,
disabledFeatures: [],
};
await measurePerformanceAsync(log, `scoutSpace:${spaceId} 'spaces.create'`, async () => {
await measurePerformanceAsync(log, `${spaceId}: 'spaces.create'`, async () => {
return kbnClient.spaces.create(spacePayload);
});

// cache saved objects ids in space
const savedObjectsCache = new Map<string, string>();

const load = async (path: string) => {
return measurePerformanceAsync(
log,
`scoutSpace:${spaceId} 'savedObjects.load'`,
async () => {
const response = await kbnClient.importExport.load(path, {
space: spaceId,
// will create new copies of saved objects with unique ids
createNewCopies: true,
});
return measurePerformanceAsync(log, `${spaceId}: 'savedObjects.load'`, async () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

simplifying message, as we know the worker index now

const response = await kbnClient.importExport.load(path, {
space: spaceId,
// will create new copies of saved objects with unique ids
createNewCopies: true,
});

const imported = (response.successResults as ImportSavedObjects[]).map(
(r: { type: string; destinationId: string; meta: { title: string } }) => {
return { id: r.destinationId, type: r.type, title: r.meta.title };
}
);
const imported = (response.successResults as ImportSavedObjects[]).map(
(r: { type: string; destinationId: string; meta: { title: string } }) => {
return { id: r.destinationId, type: r.type, title: r.meta.title };
}
);

imported.forEach((so) => savedObjectsCache.set(so.title, so.id));
imported.forEach((so) => savedObjectsCache.set(so.title, so.id));

return imported;
}
);
return imported;
});
};
const cleanStandardList = async () => {
return measurePerformanceAsync(
log,
`scoutSpace:${spaceId} 'savedObjects.cleanStandardList'`,
`${spaceId}: 'savedObjects.cleanStandardList'`,
async () => {
savedObjectsCache.clear();
await kbnClient.savedObjects.cleanStandardList({
Expand All @@ -70,7 +66,7 @@ export const scoutSpaceParallelFixture = coreWorkerFixtures.extend<
const setDefaultIndex = async (dataViewName: string) => {
return measurePerformanceAsync(
log,
`scoutSpace:${spaceId} 'savedObjects.setDefaultIndex'`,
`${spaceId}: 'savedObjects.setDefaultIndex'`,
async () => {
if (savedObjectsCache.has(dataViewName)) {
return kbnClient.uiSettings.update(
Expand All @@ -91,31 +87,23 @@ export const scoutSpaceParallelFixture = coreWorkerFixtures.extend<
});
};
const unset = async (...keys: string[]) => {
return measurePerformanceAsync(
log,
`scoutSpace:${spaceId} 'uiSettings.unset'`,
async () => {
return Promise.all(
keys.map((key) => kbnClient.uiSettings.unset(key, { space: spaceId }))
);
}
);
return measurePerformanceAsync(log, `${spaceId}: 'uiSettings.unset'`, async () => {
return Promise.all(
keys.map((key) => kbnClient.uiSettings.unset(key, { space: spaceId }))
);
});
};
const setDefaultTime = async ({ from, to }: { from: string; to: string }) => {
return measurePerformanceAsync(
log,
`scoutSpace:${spaceId} 'uiSettings.setDefaultTime'`,
async () => {
const utcFrom = isValidUTCDate(from) ? from : formatTime(from);
const untcTo = isValidUTCDate(to) ? to : formatTime(to);
return kbnClient.uiSettings.update(
{
'timepicker:timeDefaults': `{ "from": "${utcFrom}", "to": "${untcTo}"}`,
},
{ space: spaceId }
);
}
);
return measurePerformanceAsync(log, `${spaceId}: 'uiSettings.setDefaultTime'`, async () => {
const utcFrom = isValidUTCDate(from) ? from : formatTime(from);
const utcTo = isValidUTCDate(to) ? to : formatTime(to);
return kbnClient.uiSettings.update(
{
'timepicker:timeDefaults': `{ "from": "${utcFrom}", "to": "${utcTo}"}`,
},
{ space: spaceId }
);
});
};

const savedObjects = {
Expand All @@ -130,11 +118,12 @@ export const scoutSpaceParallelFixture = coreWorkerFixtures.extend<
setDefaultTime,
};

log.serviceLoaded(`scoutSpace:${spaceId}`);
log.serviceMessage('scoutSpace', `New Kibana space '${spaceId}' created`);
await use({ savedObjects, uiSettings, id: spaceId });

// Cleanup space after tests via API call
await measurePerformanceAsync(log, `scoutSpace:${spaceId} 'space.delete'`, async () => {
await measurePerformanceAsync(log, `${spaceId} 'space.delete'`, async () => {
log.debug(`Deleting space ${spaceId}`);
return kbnClient.spaces.delete(spaceId);
});
},
Expand Down