Add details to Activity events

Summary: Update Activity API to allow adding more details to events for telemetry purposes.

Reviewed By: davidaurelio

Differential Revision: D3982691

fbshipit-source-id: 07f3ed5d1ec4eddbbdeb00feb02ea75e1168705e
This commit is contained in:
Ovidiu Viorel Iepure 2016-10-12 10:35:57 -07:00 committed by Facebook Github Bot
parent 308ab1001e
commit dc0f7875c8
8 changed files with 137 additions and 35 deletions

View File

@ -11,16 +11,26 @@
*/ */
'use strict'; 'use strict';
export type EventOptions = { export type Options = {
telemetric?: boolean, telemetric?: boolean,
silent?: boolean, silent?: boolean,
displayFields?: Array<string> | true,
}; };
export type Event = { type EventFieldDescriptor = {
id: number, type: 'int' | 'normal',
startTimeStamp: [number, number], value: number | string | boolean,
durationMs?: number, };
name: string,
data?: any, export type NormalisedEventData = {[key: string]: EventFieldDescriptor};
options: EventOptions,
export type EventData = {[key: string]: number | string | boolean};
export type Event = {
data: NormalisedEventData,
durationMs?: number,
id: number,
name: string,
options: Options,
startTimeStamp: [number, number],
}; };

View File

@ -32,7 +32,7 @@ describe('Activity', () => {
const EVENT_NAME = 'EVENT_NAME'; const EVENT_NAME = 'EVENT_NAME';
const DATA = {someData: 42}; const DATA = {someData: 42};
Activity.startEvent(EVENT_NAME, DATA); Activity.startEvent(EVENT_NAME, DATA, {displayFields: ['someData']});
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
// eslint-disable-next-line no-console-disallow // eslint-disable-next-line no-console-disallow
@ -41,7 +41,7 @@ describe('Activity', () => {
const consoleMsg = console.log.mock.calls[0][0]; const consoleMsg = console.log.mock.calls[0][0];
expect(consoleMsg).toContain('START'); expect(consoleMsg).toContain('START');
expect(consoleMsg).toContain(EVENT_NAME); expect(consoleMsg).toContain(EVENT_NAME);
expect(consoleMsg).toContain(JSON.stringify(DATA)); expect(consoleMsg).toContain('someData: 42');
}); });
it('does not write the "START" phase of silent events to the console', () => { it('does not write the "START" phase of silent events to the console', () => {
@ -61,7 +61,7 @@ describe('Activity', () => {
const EVENT_NAME = 'EVENT_NAME'; const EVENT_NAME = 'EVENT_NAME';
const DATA = {someData: 42}; const DATA = {someData: 42};
const eventID = Activity.startEvent(EVENT_NAME, DATA); const eventID = Activity.startEvent(EVENT_NAME, DATA, {displayFields: ['someData']});
Activity.endEvent(eventID); Activity.endEvent(eventID);
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
@ -71,7 +71,7 @@ describe('Activity', () => {
const consoleMsg = console.log.mock.calls[1][0]; const consoleMsg = console.log.mock.calls[1][0];
expect(consoleMsg).toContain('END'); expect(consoleMsg).toContain('END');
expect(consoleMsg).toContain(EVENT_NAME); expect(consoleMsg).toContain(EVENT_NAME);
expect(consoleMsg).toContain(JSON.stringify(DATA)); expect(consoleMsg).toContain('someData: 42');
}); });
it('does not write the "END" phase of silent events to the console', () => { it('does not write the "END" phase of silent events to the console', () => {

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*
*/
'use strict';
import type {Event} from './Types';
function getDataString(event: Event): string {
const {options, data} = event;
const {displayFields} = options;
if (!Object.keys(data).length ||
!(Array.isArray(displayFields) || displayFields === true)) {
return '';
}
const fields = Array.isArray(displayFields) ? displayFields : Object.keys(data);
const dataList = fields.map(field => {
if (data[field] === undefined) {
throw new Error(`"${field}" is not defined for event ""${event.name}"!`);
}
return `${field}: ${data[field].value.toString()}`;
});
let dataString = dataList.join(' | ');
if (dataString) {
dataString = ` ${dataString} `;
}
return dataString;
}
module.exports = getDataString;

View File

@ -11,11 +11,12 @@
*/ */
'use strict'; 'use strict';
import type {EventOptions} from './Types'; import type {Event, EventData, Options} from './Types';
import type {Event} from './Types';
const chalk = require('chalk'); const chalk = require('chalk');
const events = require('events'); const events = require('events');
const formatData = require('./formatData');
const normaliseEventData = require('./normaliseEventData');
let ENABLED = true; let ENABLED = true;
let UUID = 1; let UUID = 1;
@ -23,24 +24,21 @@ let UUID = 1;
const EVENT_INDEX: {[key: number]: Event} = Object.create(null); const EVENT_INDEX: {[key: number]: Event} = Object.create(null);
const EVENT_EMITTER = new events.EventEmitter(); const EVENT_EMITTER = new events.EventEmitter();
function startEvent( function startEvent(name: string, data: EventData = {}, options: Options = {}): number {
name: string,
data: any = null,
options?: EventOptions = {},
): number {
if (name == null) { if (name == null) {
throw new Error('No event name specified!'); throw new Error('No event name specified!');
} }
const id = UUID++; const id = UUID++;
EVENT_INDEX[id] = { EVENT_INDEX[id] = {
data: normaliseEventData(data),
id, id,
startTimeStamp: process.hrtime(),
name,
data,
options, options,
name,
startTimeStamp: process.hrtime(),
}; };
logEvent(id, 'startEvent'); logEvent(id, 'startEvent');
return id; return id;
} }
@ -74,12 +72,11 @@ function logEvent(id: number, phase: 'startEvent' | 'endEvent'): void {
const { const {
name, name,
durationMs, durationMs,
data,
options, options,
} = event; } = event;
const logTimeStamp = new Date().toLocaleString(); const logTimeStamp = new Date().toLocaleString();
const dataString = data ? ': ' + JSON.stringify(data) : ''; const dataString = formatData(event);
const {telemetric, silent} = options; const {telemetric, silent} = options;
switch (phase) { switch (phase) {
@ -94,15 +91,15 @@ function logEvent(id: number, phase: 'startEvent' | 'endEvent'): void {
if (!silent) { if (!silent) {
// eslint-disable-next-line no-console-disallow // eslint-disable-next-line no-console-disallow
console.log( console.log(
chalk.dim(`[${logTimeStamp}] <END> ${name}${dataString} `) + chalk.dim(`[${logTimeStamp}] <END> ${name}${dataString}`) +
(telemetric ? chalk.reset.cyan(`(${+durationMs}ms)`) : chalk.dim(`(${+durationMs}ms)`)) (telemetric ? chalk.reset.cyan(` (${+durationMs}ms)`) : chalk.dim(` (${+durationMs}ms)`))
); );
} }
forgetEvent(id); forgetEvent(id);
break; break;
default: default:
throw new Error('Unexpected scheduled event type: ' + name); throw new Error(`Unexpected event phase "${phase}"!`);
} }
} }

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*
*/
'use strict';
import type {EventData, NormalisedEventData} from './Types';
function normaliseEventData(eventData: EventData): NormalisedEventData {
if (!eventData) {
return {};
}
const normalisedEventData = {};
Object.keys(eventData).forEach(field => {
const value = eventData[field];
let type;
if (typeof value === 'string' || typeof value === 'boolean') {
type = 'normal';
} else if (typeof value === 'number') {
type = 'int';
} else {
throw new Error(`Disallowed value for event field "${field}""!`);
}
normalisedEventData[field] = {type, value};
});
return normalisedEventData;
}
module.exports = normaliseEventData;

View File

@ -366,11 +366,12 @@ class Bundler {
}) { }) {
const findEventId = Activity.startEvent( const findEventId = Activity.startEvent(
'Transforming modules', 'Transforming modules',
null, {
entry_point: entryFile,
environment: dev ? 'dev' : 'prod',
},
{ {
telemetric: true, telemetric: true,
entryPoint: entryFile,
details: dev ? 'dev' : 'prod',
}, },
); );
const modulesByName = Object.create(null); const modulesByName = Object.create(null);

View File

@ -114,7 +114,9 @@ class Transformer {
debug('transforming file', fileName); debug('transforming file', fileName);
const transformEventId = Activity.startEvent( const transformEventId = Activity.startEvent(
'Transforming file', 'Transforming file',
fileName, {
file_name: fileName,
},
{ {
telemetric: true, telemetric: true,
silent: true, silent: true,

View File

@ -499,7 +499,15 @@ class Server {
_processAssetsRequest(req, res) { _processAssetsRequest(req, res) {
const urlObj = url.parse(decodeURI(req.url), true); const urlObj = url.parse(decodeURI(req.url), true);
const assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/); const assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/);
const assetEvent = Activity.startEvent('Processing asset request', {asset: assetPath[1]}); const assetEvent = Activity.startEvent(
'Processing asset request',
{
asset: assetPath[1],
},
{
displayFields: true,
},
);
this._assetServer.get(assetPath[1], urlObj.query.platform) this._assetServer.get(assetPath[1], urlObj.query.platform)
.then( .then(
data => { data => {
@ -538,10 +546,11 @@ class Server {
Activity.startEvent( Activity.startEvent(
'Updating existing bundle', 'Updating existing bundle',
{ {
outdatedModules: outdated.size, outdated_modules: outdated.size,
}, },
{ {
telemetric: true, telemetric: true,
displayFields: true,
}, },
); );
debug('Attempt to update existing bundle'); debug('Attempt to update existing bundle');
@ -650,11 +659,11 @@ class Server {
'Requesting bundle', 'Requesting bundle',
{ {
url: req.url, url: req.url,
entry_point: options.entryFile,
}, },
{ {
telemetric: true, telemetric: true,
entryPoint: options.entryFile, displayFields: ['url'],
details: req.url,
}, },
); );