* add a label with the number of resources actually loaded from network * code review * @v code review + tests * minor review fixes * update jest Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Liza Katz <lizka.k@gmail.com>
This commit is contained in:
parent
ba7667ebbf
commit
a0e1c46051
47
src/core/public/apm_resource_counter.ts
Normal file
47
src/core/public/apm_resource_counter.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export class CachedResourceObserver {
|
||||
private loaded = {
|
||||
networkOrDisk: 0,
|
||||
memory: 0,
|
||||
};
|
||||
private observer?: PerformanceObserver;
|
||||
|
||||
constructor() {
|
||||
if (!window.PerformanceObserver) return;
|
||||
|
||||
const cb = (entries: PerformanceObserverEntryList) => {
|
||||
const e = entries.getEntries();
|
||||
e.forEach((entry: Record<string, any>) => {
|
||||
if (entry.initiatorType === 'script' || entry.initiatorType === 'link') {
|
||||
// If the resource is fetched from a local cache, or if it is a cross-origin resource, this property returns zero.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/transferSize
|
||||
if (entry.name.indexOf(window.location.host) > -1 && entry.transferSize === 0) {
|
||||
this.loaded.memory++;
|
||||
} else {
|
||||
this.loaded.networkOrDisk++;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
this.observer = new PerformanceObserver(cb);
|
||||
this.observer.observe({
|
||||
type: 'resource',
|
||||
buffered: true,
|
||||
});
|
||||
}
|
||||
|
||||
public getCounts() {
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
}
|
|
@ -7,9 +7,11 @@
|
|||
*/
|
||||
|
||||
jest.mock('@elastic/apm-rum');
|
||||
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
|
||||
import type { DeeplyMockedKeys, MockedKeys } from '@kbn/utility-types/jest';
|
||||
import { init, apm } from '@elastic/apm-rum';
|
||||
import { ApmSystem } from './apm_system';
|
||||
import { Subject } from 'rxjs';
|
||||
import { InternalApplicationStart } from './application/types';
|
||||
|
||||
const initMock = init as jest.Mocked<typeof init>;
|
||||
const apmMock = apm as DeeplyMockedKeys<typeof apm>;
|
||||
|
@ -39,6 +41,119 @@ describe('ApmSystem', () => {
|
|||
expect(apm.addLabels).toHaveBeenCalledWith({ alpha: 'one' });
|
||||
});
|
||||
|
||||
describe('manages the page load transaction', () => {
|
||||
it('does nothing if theres no transaction', async () => {
|
||||
const apmSystem = new ApmSystem({ active: true });
|
||||
const mockTransaction: MockedKeys<Transaction> = {
|
||||
type: 'wrong',
|
||||
// @ts-expect-error 2345
|
||||
block: jest.fn(),
|
||||
mark: jest.fn(),
|
||||
};
|
||||
apmMock.getCurrentTransaction.mockReturnValue(mockTransaction);
|
||||
await apmSystem.setup();
|
||||
expect(mockTransaction.mark).not.toHaveBeenCalled();
|
||||
// @ts-expect-error 2345
|
||||
expect(mockTransaction.block).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('blocks a page load transaction', async () => {
|
||||
const apmSystem = new ApmSystem({ active: true });
|
||||
const mockTransaction: MockedKeys<Transaction> = {
|
||||
type: 'page-load',
|
||||
// @ts-expect-error 2345
|
||||
block: jest.fn(),
|
||||
mark: jest.fn(),
|
||||
};
|
||||
apmMock.getCurrentTransaction.mockReturnValue(mockTransaction);
|
||||
await apmSystem.setup();
|
||||
expect(mockTransaction.mark).toHaveBeenCalledTimes(1);
|
||||
expect(mockTransaction.mark).toHaveBeenCalledWith('apm-setup');
|
||||
// @ts-expect-error 2345
|
||||
expect(mockTransaction.block).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('marks apm start', async () => {
|
||||
const apmSystem = new ApmSystem({ active: true });
|
||||
const currentAppId$ = new Subject<string>();
|
||||
const mark = jest.fn();
|
||||
const mockTransaction: MockedKeys<Transaction> = {
|
||||
type: 'page-load',
|
||||
mark,
|
||||
// @ts-expect-error 2345
|
||||
block: jest.fn(),
|
||||
end: jest.fn(),
|
||||
addLabels: jest.fn(),
|
||||
};
|
||||
|
||||
apmMock.getCurrentTransaction.mockReturnValue(mockTransaction);
|
||||
await apmSystem.setup();
|
||||
|
||||
mark.mockReset();
|
||||
|
||||
await apmSystem.start({
|
||||
application: {
|
||||
currentAppId$,
|
||||
} as any as InternalApplicationStart,
|
||||
});
|
||||
|
||||
expect(mark).toHaveBeenCalledWith('apm-start');
|
||||
});
|
||||
|
||||
it('closes the page load transaction once', async () => {
|
||||
const apmSystem = new ApmSystem({ active: true });
|
||||
const currentAppId$ = new Subject<string>();
|
||||
const mockTransaction: MockedKeys<Transaction> = {
|
||||
type: 'page-load',
|
||||
// @ts-expect-error 2345
|
||||
block: jest.fn(),
|
||||
mark: jest.fn(),
|
||||
end: jest.fn(),
|
||||
addLabels: jest.fn(),
|
||||
};
|
||||
apmMock.getCurrentTransaction.mockReturnValue(mockTransaction);
|
||||
await apmSystem.setup();
|
||||
await apmSystem.start({
|
||||
application: {
|
||||
currentAppId$,
|
||||
} as any as InternalApplicationStart,
|
||||
});
|
||||
currentAppId$.next('myapp');
|
||||
|
||||
expect(mockTransaction.end).toHaveBeenCalledTimes(1);
|
||||
|
||||
currentAppId$.next('another-app');
|
||||
|
||||
expect(mockTransaction.end).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('adds resource load labels', async () => {
|
||||
const apmSystem = new ApmSystem({ active: true });
|
||||
const currentAppId$ = new Subject<string>();
|
||||
const mockTransaction: Transaction = {
|
||||
type: 'page-load',
|
||||
// @ts-expect-error 2345
|
||||
block: jest.fn(),
|
||||
mark: jest.fn(),
|
||||
end: jest.fn(),
|
||||
addLabels: jest.fn(),
|
||||
};
|
||||
apmMock.getCurrentTransaction.mockReturnValue(mockTransaction);
|
||||
await apmSystem.setup();
|
||||
await apmSystem.start({
|
||||
application: {
|
||||
currentAppId$,
|
||||
} as any as InternalApplicationStart,
|
||||
});
|
||||
currentAppId$.next('myapp');
|
||||
|
||||
expect(mockTransaction.addLabels).toHaveBeenCalledWith({
|
||||
'loaded-resources': 0,
|
||||
'cached-resources': 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('http request normalization', () => {
|
||||
let windowSpy: any;
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import type { ApmBase, AgentConfigOptions } from '@elastic/apm-rum';
|
||||
import { modifyUrl } from '@kbn/std';
|
||||
import { CachedResourceObserver } from './apm_resource_counter';
|
||||
import type { InternalApplicationStart } from './application';
|
||||
|
||||
/** "GET protocol://hostname:port/pathname" */
|
||||
|
@ -31,17 +32,21 @@ interface StartDeps {
|
|||
export class ApmSystem {
|
||||
private readonly enabled: boolean;
|
||||
private pageLoadTransaction?: Transaction;
|
||||
private resourceObserver: CachedResourceObserver;
|
||||
private apm?: ApmBase;
|
||||
/**
|
||||
* `apmConfig` would be populated with relevant APM RUM agent
|
||||
* configuration if server is started with elastic.apm.* config.
|
||||
*/
|
||||
constructor(private readonly apmConfig?: ApmConfig, private readonly basePath = '') {
|
||||
this.enabled = apmConfig != null && !!apmConfig.active;
|
||||
this.resourceObserver = new CachedResourceObserver();
|
||||
}
|
||||
|
||||
async setup() {
|
||||
if (!this.enabled) return;
|
||||
const { init, apm } = await import('@elastic/apm-rum');
|
||||
this.apm = apm;
|
||||
const { globalLabels, ...apmConfig } = this.apmConfig!;
|
||||
if (globalLabels) {
|
||||
apm.addLabels(globalLabels);
|
||||
|
@ -50,36 +55,23 @@ export class ApmSystem {
|
|||
this.addHttpRequestNormalization(apm);
|
||||
|
||||
init(apmConfig);
|
||||
this.pageLoadTransaction = apm.getCurrentTransaction();
|
||||
|
||||
// Keep the page load transaction open until all resources finished loading
|
||||
if (this.pageLoadTransaction && this.pageLoadTransaction.type === 'page-load') {
|
||||
// @ts-expect-error 2339
|
||||
this.pageLoadTransaction.block(true);
|
||||
this.pageLoadTransaction.mark('apm-setup');
|
||||
}
|
||||
// hold page load transaction blocks a transaction implicitly created by init.
|
||||
this.holdPageLoadTransaction(apm);
|
||||
}
|
||||
|
||||
async start(start?: StartDeps) {
|
||||
if (!this.enabled || !start) return;
|
||||
|
||||
if (this.pageLoadTransaction && this.pageLoadTransaction.type === 'page-load') {
|
||||
this.pageLoadTransaction.mark('apm-start');
|
||||
}
|
||||
this.markPageLoadStart();
|
||||
|
||||
/**
|
||||
* Register listeners for navigation changes and capture them as
|
||||
* route-change transactions after Kibana app is bootstrapped
|
||||
*/
|
||||
start.application.currentAppId$.subscribe((appId) => {
|
||||
const apmInstance = (window as any).elasticApm;
|
||||
if (appId && apmInstance && typeof apmInstance.startTransaction === 'function') {
|
||||
// Close the page load transaction
|
||||
if (this.pageLoadTransaction && this.pageLoadTransaction.type === 'page-load') {
|
||||
this.pageLoadTransaction.end();
|
||||
this.pageLoadTransaction = undefined;
|
||||
}
|
||||
apmInstance.startTransaction(`/app/${appId}`, 'route-change', {
|
||||
if (appId && this.apm) {
|
||||
this.closePageLoadTransaction();
|
||||
this.apm.startTransaction(`/app/${appId}`, 'route-change', {
|
||||
managed: true,
|
||||
canReuse: true,
|
||||
});
|
||||
|
@ -87,6 +79,39 @@ export class ApmSystem {
|
|||
});
|
||||
}
|
||||
|
||||
/* Hold the page load transaction open, until all resources actually finish loading */
|
||||
private holdPageLoadTransaction(apm: ApmBase) {
|
||||
const transaction = apm.getCurrentTransaction();
|
||||
|
||||
// Keep the page load transaction open until all resources finished loading
|
||||
if (transaction && transaction.type === 'page-load') {
|
||||
this.pageLoadTransaction = transaction;
|
||||
// @ts-expect-error 2339 block is a private property of Transaction interface
|
||||
this.pageLoadTransaction.block(true);
|
||||
this.pageLoadTransaction.mark('apm-setup');
|
||||
}
|
||||
}
|
||||
|
||||
/* Close and clear the page load transaction */
|
||||
private closePageLoadTransaction() {
|
||||
if (this.pageLoadTransaction) {
|
||||
const loadCounts = this.resourceObserver.getCounts();
|
||||
this.pageLoadTransaction.addLabels({
|
||||
'loaded-resources': loadCounts.networkOrDisk,
|
||||
'cached-resources': loadCounts.memory,
|
||||
});
|
||||
this.resourceObserver.destroy();
|
||||
this.pageLoadTransaction.end();
|
||||
this.pageLoadTransaction = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private markPageLoadStart() {
|
||||
if (this.pageLoadTransaction) {
|
||||
this.pageLoadTransaction.mark('apm-start');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an observer to the APM configuration for normalizing transactions of the 'http-request' type to remove the
|
||||
* hostname, protocol, port, and base path. Allows for coorelating data cross different deployments.
|
||||
|
|
Loading…
Reference in a new issue