Skip to content

Commit 9235bac

Browse files
authored
Add Telemetry to the task (#25)
* Log Telemetry from the Email Report Task * Fix telemetry logs
1 parent b1feb8b commit 9235bac

File tree

12 files changed

+194
-41
lines changed

12 files changed

+194
-41
lines changed

Tasks/emailReportTask/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import { DataProviderFactory } from "./providers/DataProviderFactory";
77
import { HTMLReportCreator } from "./htmlreport/HTMLReportCreator";
88
import { EmailSender } from "./EmailSender";
99
import { ReportError } from "./exceptions/ReportError";
10+
import { TelemetryLogger } from "./telemetry/TelemetryLogger";
1011

1112
async function run(): Promise<void> {
1213
try {
1314
const configProvider = new ConfigurationProvider();
1415
const reportConfiguration = new ReportConfiguration(configProvider);
1516
const reportProvider = new ReportProvider(new DataProviderFactory(configProvider.getPipelineConfiguration()));
1617

18+
// Log telemetry: Task Inputs and Configuration
19+
TelemetryLogger.LogTaskConfig(reportConfiguration);
20+
1721
const reportManager = new ReportManager(
1822
reportProvider,
1923
new HTMLReportCreator(),
@@ -27,15 +31,19 @@ async function run(): Promise<void> {
2731
console.log("Unable to set variable value in 10 sec. Exiting task.");
2832
}
2933
}
30-
console.log("Task Processing Complete.");
3134
}
3235
catch (err) {
3336
if (err instanceof ReportError) {
3437
console.log(err.getMessage());
38+
} else {
39+
console.log(err);
3540
}
3641
// Fail task
3742
throw err;
3843
}
44+
finally {
45+
console.log("Task Processing Complete.");
46+
}
3947
}
4048

4149
function sleep(ms: number): Promise<boolean> {

Tasks/emailReportTask/providers/IPostProcessor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { ReportConfiguration } from "../config/ReportConfiguration";
22
import { Report } from "../model/Report";
33

44
export interface IPostProcessor {
5-
processReportAsync(reportConfig: ReportConfiguration, finalReport: Report): Promise<void>;
5+
processReportAsync(reportConfig: ReportConfiguration, finalReport: Report): Promise<boolean>;
66
}

Tasks/emailReportTask/providers/ReportProvider.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ReportFactory } from "../model/ReportFactory";
88
import { ReportError } from "../exceptions/ReportError";
99
import { DataProviderError } from "../exceptions/DataProviderError";
1010
import { PostProcessorError } from "../exceptions/PostProcessorError";
11+
import { TelemetryLogger } from "../telemetry/TelemetryLogger";
1112

1213
export class ReportProvider implements IReportProvider {
1314

@@ -22,13 +23,13 @@ export class ReportProvider implements IReportProvider {
2223
async createReportAsync(reportConfig: ReportConfiguration): Promise<Report> {
2324
let finalReport: Report;
2425
try {
25-
const reportTaskArray = this.dataProviders.map(dataProvider => this.callDataProvider(dataProvider, reportConfig));
26+
const reportTaskArray = this.dataProviders.map(dataProvider => TelemetryLogger.InvokeWithPerfLogger<Report>(async () => this.callDataProvider(dataProvider, reportConfig), dataProvider.constructor.name));
2627

27-
const reports = await Promise.all(reportTaskArray);``
28+
const reports = await Promise.all(reportTaskArray);
2829
finalReport = ReportFactory.mergeReports(reports);
2930

3031
// Post Process data collected
31-
const processorTasks = this.postProcessors.map(processor => this.callPostProcessor(processor, reportConfig, finalReport));
32+
const processorTasks = this.postProcessors.map(processor => TelemetryLogger.InvokeWithPerfLogger<boolean>(async () => this.callPostProcessor(processor, reportConfig, finalReport), processor.constructor.name));
3233
// Wait for all processors
3334
await Promise.all(processorTasks);
3435
}
@@ -40,31 +41,39 @@ export class ReportProvider implements IReportProvider {
4041
return finalReport;
4142
}
4243

43-
private callDataProvider(dataProvider: IDataProvider, reportConfig: ReportConfiguration) : Promise<Report> {
44+
private async callDataProvider(dataProvider: IDataProvider, reportConfig: ReportConfiguration): Promise<Report> {
45+
let report: Report = null;
4446
try {
45-
return dataProvider.getReportDataAsync(reportConfig.$pipelineConfiguration, reportConfig.$reportDataConfiguration);
47+
report = await dataProvider.getReportDataAsync(reportConfig.$pipelineConfiguration, reportConfig.$reportDataConfiguration);
4648
}
4749
catch (err) {
50+
// Do not error out until all data providers are done
51+
console.log(err);
4852
if (!(err instanceof ReportError)) {
4953
const reportError = new DataProviderError(`Error fetching data using ${dataProvider.constructor.name}: ${err.message}`);
5054
reportError.innerError = err;
5155
throw reportError;
5256
}
5357
throw err;
5458
}
59+
return report;
5560
}
5661

57-
private callPostProcessor(postProcessor: IPostProcessor, reportConfig: ReportConfiguration, report: Report) : Promise<void> {
62+
private async callPostProcessor(postProcessor: IPostProcessor, reportConfig: ReportConfiguration, report: Report): Promise<boolean> {
63+
let retVal = false;
5864
try {
59-
return postProcessor.processReportAsync(reportConfig, report);
65+
retVal = await postProcessor.processReportAsync(reportConfig, report);
6066
}
6167
catch (err) {
68+
// Do not error out until all post processors are done
69+
console.log(err);
6270
if (!(err instanceof ReportError)) {
6371
const reportError = new PostProcessorError(`Error fetching data using ${postProcessor.constructor.name}: ${err.message}`);
6472
reportError.innerError = err;
6573
throw reportError;
6674
}
6775
throw err;
6876
}
77+
return retVal;
6978
}
7079
}

Tasks/emailReportTask/providers/SendMailConditionProcessor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class SendMailConditionProcessor implements IPostProcessor {
1818
this.testResultsClient = testResultsClient;
1919
}
2020

21-
public async processReportAsync(reportConfig: ReportConfiguration, report: Report): Promise<void> {
21+
public async processReportAsync(reportConfig: ReportConfiguration, report: Report): Promise<boolean> {
2222
var shouldSendMail = false;
2323
if (!report.$dataMissing) {
2424
const sendMailCondition = reportConfig.$sendMailCondition;
@@ -50,6 +50,7 @@ export class SendMailConditionProcessor implements IPostProcessor {
5050
}
5151

5252
report.$sendMailConditionSatisfied = shouldSendMail;
53+
return shouldSendMail;
5354
}
5455

5556
public async hasPreviousReleaseGotSameFailuresAsync(

Tasks/emailReportTask/providers/pipeline/BuildDataProvider.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ export class BuildDataProvider implements IDataProvider {
3232
throw new PipelineNotFoundError(`ProjectId: ${pipelineConfig.$projectId}, ${pipelineConfig.$pipelineId}`);
3333
}
3434

35-
const timeline = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineTimelineAsync(build.id));
36-
const changes = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineChangesAsync(build.id));
35+
const timeline = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineTimelineAsync(build.id), "GetBuildTimeline");
36+
const changes = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineChangesAsync(build.id), "GetPipelineChanges");
3737
const phases = this.getPhases(timeline);
3838
const lastCompletedBuild = await this.pipelineRestClient.getLastPipelineAsync(build.definition.id, null, build.sourceBranch) as Build;
39-
const lastCompletedTimeline = lastCompletedBuild != null ? await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineTimelineAsync(lastCompletedBuild.id)) : null;
39+
const lastCompletedTimeline = lastCompletedBuild != null ? await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineTimelineAsync(lastCompletedBuild.id), "GetLastCompletedTimeline") : null;
4040

4141
console.log("Fetched release data");
4242
report.setBuildData(build, timeline, lastCompletedBuild, lastCompletedTimeline, phases, changes);
@@ -45,7 +45,7 @@ export class BuildDataProvider implements IDataProvider {
4545
}
4646

4747
private async getBuildAsync(pipelineConfig: PipelineConfiguration): Promise<Build> {
48-
var build = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineAsync());
48+
var build = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineAsync(), "GetPipeline");
4949
if(isNullOrUndefined(build)) {
5050
throw new DataProviderError(`Unable to find build with id: ${pipelineConfig.$pipelineId}`);
5151
}

Tasks/emailReportTask/providers/pipeline/ReleaseDataProvider.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class ReleaseDataProvider implements IDataProvider {
4040
// check if last completed one isn't latter one, then changes don't make sense
4141
if (lastCompletedRelease != null && lastCompletedRelease.id < release.id) {
4242
console.log(`Getting changes between releases ${release.id} & ${lastCompletedRelease.id}`);
43-
changes = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineChangesAsync(lastCompletedRelease.id));
43+
changes = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineChangesAsync(lastCompletedRelease.id), "GetPipelineChanges");
4444
}
4545
else {
4646
console.log("Unable to find any last completed release");
@@ -53,7 +53,7 @@ export class ReleaseDataProvider implements IDataProvider {
5353
}
5454

5555
private async getReleaseAsync(pipelineConfig: PipelineConfiguration): Promise<Release> {
56-
var release = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineAsync());
56+
var release = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getPipelineAsync(), "GetPipeline");
5757
if(isNullOrUndefined(release)) {
5858
throw new DataProviderError(`Unable to find release with release id: ${pipelineConfig.$pipelineId}`);
5959
}
@@ -109,7 +109,7 @@ export class ReleaseDataProvider implements IDataProvider {
109109

110110
console.log(`Fetching last release by completed environment id - ${pipelineConfig.$environmentId} and branch id ${branchId}`);
111111
const lastRelease = await RetryablePromise.RetryAsync(async () => this.pipelineRestClient.getLastPipelineAsync(release.releaseDefinition.id,
112-
environment.definitionEnvironmentId, branchId, null)); //Bug in API - release.createdOn);
112+
environment.definitionEnvironmentId, branchId, null), "GetLastCompletedPipeline"); //Bug in API - release.createdOn);
113113

114114
return lastRelease as Release;
115115
}

Tasks/emailReportTask/providers/restclients/AbstractTestResultsClient.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@ export abstract class AbstractTestResultsClient extends AbstractClient implement
2323
"Microsoft.BugCategory",
2424
automatedTestName,
2525
resultId
26-
));
26+
), "QueryTestResultBugs");
2727
}
2828

2929
public async getTestResultById(testRunId: number, resultId: number): Promise<TestCaseResult> {
3030
const testApi = await this.testApiPromise;
31-
return await RetryablePromise.RetryAsync(async () => testApi.getTestResultById(this.pipelineConfig.$projectName, testRunId, resultId));
31+
return await RetryablePromise.RetryAsync(async () => testApi.getTestResultById(this.pipelineConfig.$projectName, testRunId, resultId), "GetTestResultById");
3232
}
3333

3434
public async queryTestResultsReportAsync(parameterConfig: PipelineConfiguration = null): Promise<TestResultSummary> {
3535
const config = parameterConfig != null ? parameterConfig : this.pipelineConfig;
36-
return await RetryablePromise.RetryAsync(async () => this.queryTestResultsReportForPipelineAsync(config));
36+
return await RetryablePromise.RetryAsync(async () => this.queryTestResultsReportForPipelineAsync(config), "QueryTestResultsReport");
3737
}
3838

3939
public async getTestResultOwnersAsync(resultsToFetch: TestCaseResult[]): Promise<IdentityRef[]> {
@@ -45,7 +45,7 @@ export abstract class AbstractTestResultsClient extends AbstractClient implement
4545
for (let i = 0, j = resultsToFetch.length; i < j; i += this.MaxItemsSupported) {
4646
const tempArray = resultsToFetch.slice(i, i + this.MaxItemsSupported);
4747
query.results = tempArray;
48-
tasks.push(RetryablePromise.RetryAsync(async () => testApi.getTestResultsByQuery(query, this.pipelineConfig.$projectName)));
48+
tasks.push(RetryablePromise.RetryAsync(async () => testApi.getTestResultsByQuery(query, this.pipelineConfig.$projectName), "GetTestResultOwners"));
4949
}
5050

5151
await Promise.all(tasks);
@@ -71,12 +71,12 @@ export abstract class AbstractTestResultsClient extends AbstractClient implement
7171
public async getTestResultsDetailsAsync(groupBy: string, outcomeFilters?: TestOutcome[], parameterConfig: PipelineConfiguration = null): Promise<TestResultsDetails> {
7272
const filter = this.getOutcomeFilter(outcomeFilters);
7373
const config = parameterConfig != null ? parameterConfig : this.pipelineConfig;
74-
return await RetryablePromise.RetryAsync(async () => this.getTestResultsDetailsForPipelineAsync(config, groupBy, filter));
74+
return await RetryablePromise.RetryAsync(async () => this.getTestResultsDetailsForPipelineAsync(config, groupBy, filter), "GetTestResultsDetails");
7575
}
7676

7777
public async getTestResultSummaryAsync(includeFailures: boolean, parameterConfig: PipelineConfiguration = null): Promise<TestResultSummary> {
7878
const config = parameterConfig != null ? parameterConfig : this.pipelineConfig;
79-
return await RetryablePromise.RetryAsync(async () => this.queryTestResultsReportForPipelineAsync(config, includeFailures));
79+
return await RetryablePromise.RetryAsync(async () => this.queryTestResultsReportForPipelineAsync(config, includeFailures), "GetTestResultSummary");
8080
}
8181

8282
public async getTestResultsByQueryAsync(query: TestResultsQuery): Promise<TestResultsQuery> {

Tasks/emailReportTask/providers/restclients/BuildClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class BuildRestClient extends AbstractClient implements IPipelineRestClie
3030
null,
3131
null,
3232
BuildResult.Succeeded | BuildResult.PartiallySucceeded | BuildResult.Failed | BuildResult.Canceled,
33-
null,
33+
null,
3434
null,
3535
1,
3636
null,
Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,38 @@
1+
import { TelemetryLogger } from "../../telemetry/TelemetryLogger";
2+
const { performance } = require('perf_hooks');
3+
14
export class RetryablePromise {
25

36
private static defaultRetryCount: number = 3;
47

5-
public static async RetryAsync<T>(executor: () => Promise<T>, times: number = this.defaultRetryCount): Promise<T> {
8+
public static async RetryAsync<T>(executor: () => Promise<T>, executorName: string, times: number = this.defaultRetryCount): Promise<T> {
9+
const perfStart = performance.now();
610
let attemptNumber = 1;
711
let lastError: Error;
8-
do {
9-
try {
10-
let returnVal = await executor();
11-
if(attemptNumber > 1) {
12-
console.log(`Completed on Retry attempt: ${attemptNumber}`);
12+
try {
13+
do {
14+
try {
15+
let returnVal = await executor();
16+
if (attemptNumber > 1) {
17+
console.log(`Completed on Retry attempt: ${attemptNumber}`);
18+
}
19+
return returnVal;
1320
}
14-
return returnVal;
15-
}
16-
catch (err) {
17-
lastError = err;
18-
console.log(`Retry: ${attemptNumber} : ${err}`);
19-
}
20-
attemptNumber++;
21-
} while (attemptNumber <= times);
21+
catch (err) {
22+
lastError = err;
23+
console.log(`Retry: ${attemptNumber} : ${err}`);
24+
}
25+
attemptNumber++;
26+
} while (attemptNumber <= times);
2227

23-
console.log(`All Retries exhausted. Throwing error: ${lastError}`);
24-
throw lastError;
28+
console.log(`All Retries exhausted. Throwing error: ${lastError}`);
29+
throw lastError;
30+
}
31+
finally {
32+
if (attemptNumber > 1) {
33+
// Log time taken after all retries
34+
TelemetryLogger.LogModulePerf(executorName, performance.now() - perfStart);
35+
}
36+
}
2537
}
2638
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { ReportConfiguration } from "../config/ReportConfiguration";
2+
import { PipelineType } from "../config/pipeline/PipelineType";
3+
import { EnumUtils } from "../utils/EnumUtils";
4+
const { performance } = require('perf_hooks');
5+
6+
export class TelemetryLogger {
7+
public static readonly TELEMETRY_LINE =
8+
"##vso[telemetry.publish area=AgentTasks;feature=EmailReportTask]";
9+
10+
private static instance: TelemetryLogger;
11+
private static reportConfig: ReportConfiguration;
12+
13+
/**
14+
* Formats and sends all telemetry collected to be published
15+
*/
16+
public static LogTaskConfig(reportConfiguration: ReportConfiguration): void {
17+
this.reportConfig = reportConfiguration;
18+
19+
const pipelineConfig = this.reportConfig.$pipelineConfiguration;
20+
const mailConfig = this.reportConfig.$mailConfiguration;
21+
const reportDataConfig = this.reportConfig.$reportDataConfiguration;
22+
23+
let pipelineTypeString: string = "Release";
24+
let environmentId: number = 0;
25+
if (pipelineConfig.$pipelineType == PipelineType.Build) {
26+
pipelineTypeString = "Build";
27+
} else {
28+
environmentId = pipelineConfig.$environmentId;
29+
}
30+
31+
const groupTestSummary: string[] = reportDataConfig.$groupTestSummaryBy.map(g => EnumUtils.GetGroupTestResultsByString(g));
32+
let groupTestSummaryString = groupTestSummary[0];
33+
if (groupTestSummary.length > 0) {
34+
groupTestSummaryString = groupTestSummary.join(",");
35+
}
36+
37+
this.logTelemetry({
38+
pipelineId: pipelineConfig.$pipelineId,
39+
pipelineType: pipelineTypeString,
40+
projectId: pipelineConfig.$projectId,
41+
projectName: pipelineConfig.$projectName,
42+
environmentId: environmentId,
43+
taskConfiguration: {
44+
sendMailCondition: EnumUtils.GetMailConditionString(this.reportConfig.$sendMailCondition),
45+
smtpHost: mailConfig.$smtpConfig.$smtpHost,
46+
smtpUserName: mailConfig.$smtpConfig.$userName,
47+
enableTLs: mailConfig.$smtpConfig.$enableTLS,
48+
includeCommits: reportDataConfig.$includeCommits,
49+
includeOthersInTotal: reportDataConfig.$includeOthersInTotal,
50+
groupTestSummaryBy: groupTestSummaryString,
51+
testResultsConfiguration: {
52+
includeFailedTests: reportDataConfig.$testResultsConfig.$includeFailedTests,
53+
includeInconclusiveTests: reportDataConfig.$testResultsConfig.$includeInconclusiveTests,
54+
includeNotExecutedTests: reportDataConfig.$testResultsConfig.$includeNotExecutedTests,
55+
includeOtherTests: reportDataConfig.$testResultsConfig.$includeOtherTests,
56+
includePassedTests: reportDataConfig.$testResultsConfig.$includePassedTests,
57+
maxItemsToShow: reportDataConfig.$testResultsConfig.$maxItemsToShow
58+
}
59+
}
60+
});
61+
}
62+
63+
public static LogModulePerf(moduleName: string, timeTaken: number, numRetries: Number = 0) {
64+
const timeTakenString = timeTaken.toFixed(2);
65+
if (numRetries < 1) {
66+
this.logTelemetry({
67+
"ModuleName": `${moduleName}`,
68+
"PERF": `${timeTakenString}`
69+
});
70+
} else {
71+
this.logTelemetry({
72+
"ModuleName": `${moduleName}`,
73+
"PERF": `${timeTakenString}`,
74+
"Retries": `${numRetries}`
75+
});
76+
}
77+
}
78+
79+
/**
80+
* Publishes an object as a string as telemetry
81+
* @param telemetryToLog Object to be logged as a string
82+
*/
83+
private static logTelemetry(telemetryToLog: {}) {
84+
console.log(
85+
TelemetryLogger.TELEMETRY_LINE + JSON.stringify(telemetryToLog)
86+
);
87+
}
88+
89+
public static async InvokeWithPerfLogger<T>(executor: () => Promise<T>, executorName: string): Promise<T> {
90+
const perfStart = performance.now();
91+
let returnVal: T;
92+
try {
93+
returnVal = await executor();
94+
}
95+
finally {
96+
// Log time taken by the dataprovider
97+
TelemetryLogger.LogModulePerf(executorName, performance.now() - perfStart);
98+
}
99+
return returnVal;
100+
}
101+
}

0 commit comments

Comments
 (0)