diff --git a/deploy/service-bus/deploy.config.js b/deploy/service-bus/deploy.config.js index 3cf29864f7..6dca4c3fe5 100644 --- a/deploy/service-bus/deploy.config.js +++ b/deploy/service-bus/deploy.config.js @@ -91,6 +91,10 @@ const config = { { name: 'sync-results-to-db-complete', maxSizeInMegabytes: {}.hasOwnProperty.call(process.env, 'SERVICE_BUS_QUEUE_MAX_SIZE_MEGABYTES_SYNC_RESULTS_COMPLETE') ? parseInt(process.env.SERVICE_BUS_QUEUE_MAX_SIZE_MEGABYTES_SYNC_RESULTS_COMPLETE, 10) : twentyGigabytes + }, + { + name: 'ps-report-log', + maxSizeInMegabytes: {}.hasOwnProperty.call(process.env, 'SERVICE_BUS_QUEUE_MAX_SIZE_MEGABYTES_PS_REPORT_LOG') ? parseInt(process.env.SERVICE_BUS_QUEUE_MAX_SIZE_MEGABYTES_PS_REPORT_LOG, 10) : twentyGigabytes } ] } diff --git a/docs/support-utils/util-gen-ps-report-logs.md b/docs/support-utils/util-gen-ps-report-logs.md new file mode 100644 index 0000000000..460a8b5a47 --- /dev/null +++ b/docs/support-utils/util-gen-ps-report-logs.md @@ -0,0 +1,13 @@ +# util-gen-ps-report-logs + +generates fake log messages onto the `ps-report-log` service bus queue for the purpose of testing the log writer function (ps-report-log-writer). + +You can override the default message count by providing the following request body in `JSON`... + +``` +{ + messageCount: 100000 +} +``` + +Execute a `HTTP GET` at http://localhost:7071/api/util-gen-ps-report-logs to invoke the util function. diff --git a/func-consumption/util-gen-ps-report-logs/function.json b/func-consumption/util-gen-ps-report-logs/function.json new file mode 100644 index 0000000000..fd701b79a3 --- /dev/null +++ b/func-consumption/util-gen-ps-report-logs/function.json @@ -0,0 +1,21 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get" + ] + }, + { + "direction": "out", + "type": "serviceBus", + "name": "psReportLogQueue", + "queueName": "ps-report-log", + "connection": "AZURE_SERVICE_BUS_CONNECTION_STRING" + } + ], + "scriptFile": "../dist/functions/util-gen-ps-report-logs/index.js" +} diff --git a/func-ps-report/ps-report-1-list-schools/function.json b/func-ps-report/ps-report-1-list-schools/function.json index 439f57a5f2..d813fbabd0 100644 --- a/func-ps-report/ps-report-1-list-schools/function.json +++ b/func-ps-report/ps-report-1-list-schools/function.json @@ -16,6 +16,13 @@ "name": "schoolMessages", "queueName": "ps-report-schools", "connection": "AZURE_SERVICE_BUS_CONNECTION_STRING" + }, + { + "direction": "out", + "type": "serviceBus", + "name": "logs", + "queueName": "ps-report-log", + "connection": "AZURE_SERVICE_BUS_CONNECTION_STRING" } ], "scriptFile": "../dist/functions-ps-report/ps-report-1-list-schools/index.js" diff --git a/func-ps-report/ps-report-2-pupil-data/function.json b/func-ps-report/ps-report-2-pupil-data/function.json index 98fdd48983..7103d65267 100644 --- a/func-ps-report/ps-report-2-pupil-data/function.json +++ b/func-ps-report/ps-report-2-pupil-data/function.json @@ -13,6 +13,13 @@ "direction": "out", "queueName": "ps-report-staging", "connection": "AZURE_SERVICE_BUS_CONNECTION_STRING" + }, + { + "direction": "out", + "type": "serviceBus", + "name": "logs", + "queueName": "ps-report-log", + "connection": "AZURE_SERVICE_BUS_CONNECTION_STRING" } ], "scriptFile": "../dist/functions-ps-report/ps-report-2-pupil-data/index.js" diff --git a/func-ps-report/ps-report-3-transformer/function.json b/func-ps-report/ps-report-3-transformer/function.json index ad8e933097..645cca05fa 100644 --- a/func-ps-report/ps-report-3-transformer/function.json +++ b/func-ps-report/ps-report-3-transformer/function.json @@ -13,6 +13,13 @@ "name": "outputData", "queueName": "ps-report-export", "connection": "AZURE_SERVICE_BUS_CONNECTION_STRING" + }, + { + "direction": "out", + "type": "serviceBus", + "name": "logs", + "queueName": "ps-report-log", + "connection": "AZURE_SERVICE_BUS_CONNECTION_STRING" } ], "scriptFile": "../dist/functions-ps-report/ps-report-3-transformer/index.js" diff --git a/func-ps-report/ps-report-4-writer/function.json b/func-ps-report/ps-report-4-writer/function.json index 0801c2705e..d316c4d474 100644 --- a/func-ps-report/ps-report-4-writer/function.json +++ b/func-ps-report/ps-report-4-writer/function.json @@ -6,6 +6,13 @@ "direction": "in", "queueName": "ps-report-export", "connection": "AZURE_SERVICE_BUS_CONNECTION_STRING" + }, + { + "direction": "out", + "type": "serviceBus", + "name": "logs", + "queueName": "ps-report-log", + "connection": "AZURE_SERVICE_BUS_CONNECTION_STRING" } ], "scriptFile": "../dist/functions-ps-report/ps-report-4-writer/index.js" diff --git a/func-ps-report/ps-report-5-log-writer/function.json b/func-ps-report/ps-report-5-log-writer/function.json new file mode 100644 index 0000000000..91f101e76d --- /dev/null +++ b/func-ps-report/ps-report-5-log-writer/function.json @@ -0,0 +1,15 @@ +{ + "bindings": [ + { + "name": "psReport5LogWriter", + "type": "timerTrigger", + "direction": "in", + "timerDescription": "{second} {minute} {hour} {day} {month} {day-of-week}", + "dev-schedule": "0 */5 * * * *", + "prod-schedule": "0 0 7 * * 3", + "prod-description": "Every Wednesday at 7am", + "schedule": "0 0 7 * * 3" + } + ], + "scriptFile": "../dist/functions-ps-report/ps-report-5-log-writer/index.js" +} diff --git a/tslib/src/azure/blob-service.ts b/tslib/src/azure/blob-service.ts index b3a14e4585..d872db834d 100644 --- a/tslib/src/azure/blob-service.ts +++ b/tslib/src/azure/blob-service.ts @@ -3,9 +3,27 @@ import config from '../config' export interface IBlobService { deleteBlob (blobName: string, containerName: string): Promise + createBlob (data: Buffer, blobName: string, containerName: string): Promise + appendBlob (data: Buffer, blobName: string, containerName: string): Promise } export class BlobService implements IBlobService { + async appendBlob (data: Buffer, blobName: string, containerName: string): Promise { + const client = await this.getContainerClient(containerName) + await client.createIfNotExists() + const blobClient = client.getAppendBlobClient(blobName) + await blobClient.createIfNotExists() + await blobClient.appendBlock(data, data.length) + } + + async createBlob (data: Buffer, blobName: string, containerName: string): Promise { + const client = await this.getContainerClient(containerName) + await client.createIfNotExists() + const blobClient = client.getBlobClient(blobName) + const blockClient = await blobClient.getBlockBlobClient() + await blockClient.uploadData(data) + } + private async getContainerClient (containerName: string): Promise { const bsc = BlobServiceClient.fromConnectionString(config.AzureStorage.ConnectionString) return bsc.getContainerClient(containerName) diff --git a/tslib/src/common/ContextLike.ts b/tslib/src/common/ContextLike.ts new file mode 100644 index 0000000000..8f30009e10 --- /dev/null +++ b/tslib/src/common/ContextLike.ts @@ -0,0 +1,17 @@ +import { ILogger } from './logger' + +/** + * @description same signature as Azure Function ContextBindings + */ +export interface IContextBindingsLike { + [name: string]: any +} + +/** + * @description Azure Function Context Like object that supports members required + * by the Ps Report Logger + */ +export interface IContextLike { + bindings: IContextBindingsLike + log: ILogger +} diff --git a/tslib/src/config.ts b/tslib/src/config.ts index 0528e9ef9d..06b4f7496a 100644 --- a/tslib/src/config.ts +++ b/tslib/src/config.ts @@ -135,5 +135,8 @@ export default { SyncResultsInit: { MaxParallelTasks: parseInt(parser.valueOrSubstitute(process.env.SYNC_RESULTS_INIT_MAX_PARALLEL_TASKS, 5), 10) }, - LiveFormQuestionCount: getLinesPerCheck() + LiveFormQuestionCount: getLinesPerCheck(), + PsReportLogWriter: { + MessagesPerBatch: parseInt(parser.valueOrSubstitute(process.env.PS_REPORT_LOG_WRITER_MESSAGE_BATCH_SIZE, 100), 10) + } } diff --git a/tslib/src/functions-ps-report/common/ps-report-log-entry.ts b/tslib/src/functions-ps-report/common/ps-report-log-entry.ts new file mode 100644 index 0000000000..030cca4bda --- /dev/null +++ b/tslib/src/functions-ps-report/common/ps-report-log-entry.ts @@ -0,0 +1,17 @@ +import moment from 'moment' + +export interface IPsReportLogEntry { + message: string + level: PsReportLogLevel + source: PsReportSource + generatedAt: moment.Moment +} + +export enum PsReportSource { + SchoolGenerator = 'ps-report-1-list-schools', + PupilGenerator = 'ps-report-2-pupil-data', + Transformer = 'ps-report-3-transformer', + Writer = 'ps-report-4-writer' +} + +export type PsReportLogLevel = 'info' | 'warning' | 'verbose' | 'error' diff --git a/tslib/src/functions-ps-report/common/ps-report-logger.ts b/tslib/src/functions-ps-report/common/ps-report-logger.ts new file mode 100644 index 0000000000..510dff5990 --- /dev/null +++ b/tslib/src/functions-ps-report/common/ps-report-logger.ts @@ -0,0 +1,51 @@ +import moment from 'moment' +import { IContextLike } from '../../common/ContextLike' +import { ILogger } from '../../common/logger' +import { PsLogEntryFormatter } from '../ps-report-5-log-writer/log-entry-formatter' +import { IPsReportLogEntry, PsReportLogLevel, PsReportSource } from './ps-report-log-entry' + +const formatter = new PsLogEntryFormatter() + +export class PsReportLogger implements ILogger { + private readonly context: IContextLike + private readonly source: PsReportSource + + constructor (context: IContextLike, sourceFunction: PsReportSource) { + this.context = context + this.source = sourceFunction + } + + private log (message: string, level: PsReportLogLevel): string { + const entry: IPsReportLogEntry = { + generatedAt: moment(), + message: message, + source: this.source, + level: level + } + if (this.context.bindings.logs === undefined) { + this.context.bindings.logs = [] + } + this.context.bindings.logs.push(entry) + return formatter.formatEntry(entry) + } + + info (message: string): void { + const formatted = this.log(message, 'info') + this.context.log.info(formatted) + } + + verbose (message: string): void { + const formatted = this.log(message, 'verbose') + this.context.log.verbose(formatted) + } + + warn (message: string): void { + const formatted = this.log(message, 'warning') + this.context.log.warn(formatted) + } + + error (message: string): void { + const formatted = this.log(message, 'error') + this.context.log.error(formatted) + } +} diff --git a/tslib/src/functions-ps-report/ps-report-1-list-schools/index.ts b/tslib/src/functions-ps-report/ps-report-1-list-schools/index.ts index 8c922e4e2a..c43c593149 100644 --- a/tslib/src/functions-ps-report/ps-report-1-list-schools/index.ts +++ b/tslib/src/functions-ps-report/ps-report-1-list-schools/index.ts @@ -2,6 +2,8 @@ import { AzureFunction, Context } from '@azure/functions' import { performance } from 'perf_hooks' import { ListSchoolsService } from './list-schools-service' import { IFunctionTimer } from '../../azure/functions' +import { PsReportLogger } from '../common/ps-report-logger' +import { PsReportSource } from '../common/ps-report-log-entry' const functionName = 'ps-report-1-list-schools' @@ -11,21 +13,21 @@ const timerTrigger: AzureFunction = async function (context: Context, timer: IFu context.log(`${functionName}: timer is past due, exiting.`) return } + const logger = new PsReportLogger(context, PsReportSource.SchoolGenerator) const start = performance.now() const meta = { processCount: 0, errorCount: 0 } try { - const schoolListService = new ListSchoolsService(context.log) + const schoolListService = new ListSchoolsService(logger) const messages = await schoolListService.getSchoolMessages() context.bindings.schoolMessages = messages meta.processCount = messages.length } catch (error) { - context.log.error(`${functionName}: ERROR: ${error.message}`) + logger.error(error.message) throw error } const end = performance.now() const durationInMilliseconds = end - start - const timeStamp = new Date().toISOString() - context.log(`${functionName}: ${timeStamp} processed ${meta.processCount} records, run took ${durationInMilliseconds} ms`) + logger.info(`processed ${meta.processCount} records, run took ${durationInMilliseconds} ms`) } export default timerTrigger diff --git a/tslib/src/functions-ps-report/ps-report-1-list-schools/list-schools-service.spec.ts b/tslib/src/functions-ps-report/ps-report-1-list-schools/list-schools-service.spec.ts index 8e28274133..682560e5a9 100644 --- a/tslib/src/functions-ps-report/ps-report-1-list-schools/list-schools-service.spec.ts +++ b/tslib/src/functions-ps-report/ps-report-1-list-schools/list-schools-service.spec.ts @@ -1,6 +1,6 @@ import { ListSchoolsService } from './list-schools-service' -import { MockLogger, ILogger } from '../../common/logger' import { ISqlService } from '../../sql/sql.service' +import { ILogger, MockLogger } from '../../common/logger' describe('ListSchoolsService', () => { let sut: ListSchoolsService diff --git a/tslib/src/functions-ps-report/ps-report-1-list-schools/list-schools-service.ts b/tslib/src/functions-ps-report/ps-report-1-list-schools/list-schools-service.ts index 9006b9a654..a983d3f631 100644 --- a/tslib/src/functions-ps-report/ps-report-1-list-schools/list-schools-service.ts +++ b/tslib/src/functions-ps-report/ps-report-1-list-schools/list-schools-service.ts @@ -1,8 +1,6 @@ -import { ConsoleLogger, ILogger } from '../../common/logger' +import { ILogger } from '../../common/logger' import { ISqlService, SqlService } from '../../sql/sql.service' -const functionName = 'ps-report-1-list-schools' - export interface School { id: number uuid: string @@ -22,8 +20,8 @@ export class ListSchoolsService implements IListSchoolsService { private readonly logger: ILogger private readonly sqlService: ISqlService - constructor (logger?: ILogger, sqlService?: ISqlService) { - this.logger = logger ?? new ConsoleLogger() + constructor (logger: ILogger, sqlService?: ISqlService) { + this.logger = logger this.sqlService = sqlService ?? new SqlService() } @@ -33,7 +31,7 @@ export class ListSchoolsService implements IListSchoolsService { } public async getSchoolMessages (): Promise { - this.logger.verbose(`${functionName}: ListSchoolsService called - retrieving all schools`) + this.logger.verbose('ListSchoolsService called - retrieving all schools') const schools = await this.getSchools() const schoolMessages: SchoolMessage[] = schools.map(school => { return { @@ -41,7 +39,7 @@ export class ListSchoolsService implements IListSchoolsService { name: school.name } }) - this.logger.info(`${functionName}: getSchoolMessages() retrieved ${schoolMessages.length} schools`) + this.logger.info(`getSchoolMessages() retrieved ${schoolMessages.length} schools`) return schoolMessages } } diff --git a/tslib/src/functions-ps-report/ps-report-2-pupil-data/index.ts b/tslib/src/functions-ps-report/ps-report-2-pupil-data/index.ts index 6c0a798c21..f69ba99618 100644 --- a/tslib/src/functions-ps-report/ps-report-2-pupil-data/index.ts +++ b/tslib/src/functions-ps-report/ps-report-2-pupil-data/index.ts @@ -2,8 +2,8 @@ import { AzureFunction, Context } from '@azure/functions' import { performance } from 'perf_hooks' import { PsReportService } from './ps-report.service' import { PupilResult } from './models' - -const functionName = 'ps-report-2-pupil-data' +import { PsReportLogger } from '../common/ps-report-logger' +import { PsReportSource } from '../common/ps-report-log-entry' /** * Incoming message is just the name and UUID of the school to process @@ -17,15 +17,15 @@ interface IncomingMessage { const serviceBusQueueTrigger: AzureFunction = async function (context: Context, incomingMessage: IncomingMessage): Promise { const start = performance.now() - context.log.verbose(`${functionName}: called for school ${incomingMessage.name}`) + const logger = new PsReportLogger(context, PsReportSource.PupilGenerator) + logger.verbose(`called for school ${incomingMessage.name}`) const outputBinding: PupilResult[] = [] context.bindings.psReportPupilMessage = outputBinding - const psReportService = new PsReportService(outputBinding, context.log) + const psReportService = new PsReportService(outputBinding, logger) await psReportService.process(incomingMessage.uuid) const end = performance.now() const durationInMilliseconds = end - start - const timeStamp = new Date().toISOString() - context.log.info(`${functionName}: ${timeStamp} processed ${outputBinding.length} pupils, run took ${durationInMilliseconds} ms`) + logger.info(`processed ${outputBinding.length} pupils, run took ${durationInMilliseconds} ms`) } export default serviceBusQueueTrigger diff --git a/tslib/src/functions-ps-report/ps-report-2-pupil-data/ps-report.data.service.ts b/tslib/src/functions-ps-report/ps-report-2-pupil-data/ps-report.data.service.ts index d601fd9bea..5967244213 100644 --- a/tslib/src/functions-ps-report/ps-report-2-pupil-data/ps-report.data.service.ts +++ b/tslib/src/functions-ps-report/ps-report-2-pupil-data/ps-report.data.service.ts @@ -1,5 +1,4 @@ import { ISqlService, SqlService } from '../../sql/sql.service' -import { ConsoleLogger, ILogger } from '../../common/logger' import { TYPES } from 'mssql' import { Answer, @@ -22,6 +21,7 @@ import { } from './models' import * as R from 'ramda' import moment from 'moment' +import { ILogger } from '../../common/logger' const functionName = 'ps-report-2-pupil-data' @@ -34,12 +34,12 @@ export interface IPsReportDataService { } export class PsReportDataService { - private readonly logger: ILogger private readonly sqlService: ISqlService + private readonly logger: ILogger private readonly checkFormCache: Map = new Map() - constructor (logger?: ILogger, sqlService?: ISqlService) { - this.logger = logger ?? new ConsoleLogger() + constructor (logger: ILogger, sqlService?: ISqlService) { + this.logger = logger this.sqlService = sqlService ?? new SqlService(this.logger) } diff --git a/tslib/src/functions-ps-report/ps-report-2-pupil-data/ps-report.service.ts b/tslib/src/functions-ps-report/ps-report-2-pupil-data/ps-report.service.ts index 8422c12769..d8aff6b3ba 100644 --- a/tslib/src/functions-ps-report/ps-report-2-pupil-data/ps-report.service.ts +++ b/tslib/src/functions-ps-report/ps-report-2-pupil-data/ps-report.service.ts @@ -1,16 +1,15 @@ -import { ConsoleLogger, ILogger } from '../../common/logger' import { IPsReportDataService, PsReportDataService } from './ps-report.data.service' import { Pupil, PupilResult, School } from './models' -const functionName = 'ps-report-2-pupil-data' +import { ILogger } from '../../common/logger' export class PsReportService { - private readonly logger: ILogger private readonly dataService: IPsReportDataService private readonly outputBinding: PupilResult[] + private readonly logger: ILogger constructor (outputBinding: any[], logger: ILogger, dataService?: IPsReportDataService) { this.outputBinding = outputBinding - this.logger = logger ?? new ConsoleLogger() + this.logger = logger this.dataService = dataService ?? new PsReportDataService(this.logger) } @@ -20,7 +19,7 @@ export class PsReportService { try { pupils = await this.dataService.getPupils(schoolUuid) } catch (error) { - this.logger.error(`${functionName} ERROR - unable to fetch pupils for school ${schoolUuid}`) + this.logger.error(`ERROR - unable to fetch pupils for school ${schoolUuid}`) throw error } for (let i = 0; i < pupils.length; i++) { @@ -34,7 +33,7 @@ export class PsReportService { this.outputBinding.push(result) } catch (error) { // Ignore the error on the particular pupil and carry on so it reports on the rest of the school - this.logger.error(`${functionName} ERROR: Failed to retrieve pupil data for pupil ${pupil.slug} in school ${schoolUuid} + this.logger.error(`ERROR: Failed to retrieve pupil data for pupil ${pupil.slug} in school ${schoolUuid} Error was ${error.message}`) } } diff --git a/tslib/src/functions-ps-report/ps-report-3-transformer/index.ts b/tslib/src/functions-ps-report/ps-report-3-transformer/index.ts index 7ed90f710f..73ee16cd65 100644 --- a/tslib/src/functions-ps-report/ps-report-3-transformer/index.ts +++ b/tslib/src/functions-ps-report/ps-report-3-transformer/index.ts @@ -3,7 +3,8 @@ import { performance } from 'perf_hooks' import { ReportLine } from './report-line.class' import { jsonReviver } from '../../common/json-reviver' import { PupilResult } from '../../functions-ps-report/ps-report-2-pupil-data/models' -const functionName = 'ps-report-transformer' +import { PsReportLogger } from '../common/ps-report-logger' +import { PsReportSource } from '../common/ps-report-log-entry' /** * This functions receives a message from a sb queue and outputs a message to a sb queue that will later be written @@ -18,7 +19,8 @@ const functionName = 'ps-report-transformer' const serviceBusQueueTrigger: AzureFunction = async function (context: Context, inputData: PupilResult): Promise { const start = performance.now() - context.log.info(`${functionName}: message received for pupil ${inputData.pupil.slug}`) + const logger = new PsReportLogger(context, PsReportSource.Transformer) + logger.info(`message received for pupil ${inputData.pupil.slug}`) try { /** * The inputData type is not absolutely correctly typed. The Moment datetime's are still strings as the JSON parsing happens @@ -36,14 +38,13 @@ const serviceBusQueueTrigger: AzureFunction = async function (context: Context, const outputData = reportLine.transform() context.bindings.outputData = outputData } catch (error) { - context.log.error(`${functionName}: ERROR: ${error.message}`) + logger.error(`ERROR: ${error.message}`) throw error } const end = performance.now() const durationInMilliseconds = end - start - const timeStamp = new Date().toISOString() - context.log(`${functionName}: ${timeStamp} run complete: ${durationInMilliseconds} ms`) + logger.info(`run complete: ${durationInMilliseconds} ms`) } /** diff --git a/tslib/src/functions-ps-report/ps-report-4-writer/index.ts b/tslib/src/functions-ps-report/ps-report-4-writer/index.ts index afa149afbc..0b9686a5a3 100644 --- a/tslib/src/functions-ps-report/ps-report-4-writer/index.ts +++ b/tslib/src/functions-ps-report/ps-report-4-writer/index.ts @@ -3,26 +3,27 @@ import { performance } from 'perf_hooks' import { IPsychometricReportLine } from '../ps-report-3-transformer/models' import { PsReportWriterService } from './ps-report-writer.service' import { jsonReviver } from '../../common/json-reviver' - -const functionName = 'ps-report-4-writer' +import { PsReportLogger } from '../common/ps-report-logger' +import { PsReportSource } from '../common/ps-report-log-entry' const serviceBusQueueTrigger: AzureFunction = async function (context: Context, incomingMessage: IPsychometricReportLine): Promise { const start = performance.now() const messageWithDates = revive(incomingMessage) - context.log.verbose(`Message received for pupil ${messageWithDates.PupilID} in school ${messageWithDates.SchoolName}`) + const logger = new PsReportLogger(context, PsReportSource.Writer) + + logger.verbose(`Message received for pupil ${messageWithDates.PupilID} in school ${messageWithDates.SchoolName}`) - const reportWriter = new PsReportWriterService(context.log) + const reportWriter = new PsReportWriterService(logger) try { await reportWriter.write(messageWithDates) } catch (error) { - context.log.error(`Failed to write data for pupil ${messageWithDates.PupilID} + logger.error(`Failed to write data for pupil ${messageWithDates.PupilID} ERROR: ${error.message}`) throw error } const end = performance.now() const durationInMilliseconds = end - start - const timeStamp = new Date().toISOString() - context.log.info(`${functionName}: ${timeStamp} processed 1 pupil, run took ${durationInMilliseconds} ms`) + logger.info(`processed 1 pupil, run took ${durationInMilliseconds} ms`) } /** diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/index.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/index.ts new file mode 100644 index 0000000000..b430248414 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/index.ts @@ -0,0 +1,74 @@ +import { AzureFunction, Context } from '@azure/functions' +import { performance } from 'perf_hooks' +import { IFunctionTimer } from '../../azure/functions' +import config from '../../config' +import * as sb from '@azure/service-bus' +import * as RA from 'ramda-adjunct' +import { LogService } from './log.service' +import moment from 'moment' + +const functionName = 'ps-report-log-generator' +const queueName = 'ps-report-log' + +const funcImplementation: AzureFunction = async function (context: Context, timer: IFunctionTimer): Promise { + if (timer.isPastDue) { + context.log(`${functionName}: timer is past due, exiting.`) + return + } + const start = performance.now() + + if (config.ServiceBus.ConnectionString === undefined) { + throw new Error(`${functionName}: ServiceBusConnection env var is missing`) + } + + let busClient: sb.ServiceBusClient + let receiver: sb.ServiceBusReceiver + + const disconnect = async (): Promise => { + await receiver.close() + await busClient.close() + } + + try { + context.log(`${functionName}: connecting to service bus...`) + busClient = new sb.ServiceBusClient(config.ServiceBus.ConnectionString) + receiver = busClient.createReceiver(queueName, { + receiveMode: 'receiveAndDelete' + }) + context.log(`${functionName}: connected to service bus instance ${busClient.fullyQualifiedNamespace}`) + } catch (error) { + context.log.error(`${functionName}: unable to connect to service bus at this time:${error.message}`) + throw error + } + + const setId = `${moment().format('YYYYMMDDHHmmss')}` + let messageBatch = new Array() + context.log(`${functionName}: attempting to process log set ${setId}...`) + try { + messageBatch = await receiver.receiveMessages(config.PsReportLogWriter.MessagesPerBatch) + let messageCount = 0 + while (!RA.isNilOrEmpty(messageBatch)) { + context.log(`${functionName}: adding ${messageBatch.length} log messages...`) + const logService = new LogService() + await logService.create(setId, messageBatch) + messageCount += messageBatch.length + messageBatch = await receiver.receiveMessages(config.PsReportLogWriter.MessagesPerBatch) + } + context.log(`${functionName}: processed ${messageCount} messages...`) + await disconnect() + finish(start, context) + return + } catch (error) { + context.log.error(error) + throw error + } +} + +function finish (start: number, context: Context): void { + const end = performance.now() + const durationInMilliseconds = end - start + const timeStamp = new Date().toISOString() + context.log(`${functionName}: ${timeStamp} run complete: ${durationInMilliseconds} ms`) +} + +export default funcImplementation diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry-formatter.spec.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry-formatter.spec.ts new file mode 100644 index 0000000000..d8486036c2 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry-formatter.spec.ts @@ -0,0 +1,23 @@ +import moment from 'moment' +import { IPsReportLogEntry, PsReportSource } from '../common/ps-report-log-entry' +import { PsLogEntryFormatter } from './log-entry-formatter' + +let sut: PsLogEntryFormatter + +describe('log entry formatter', () => { + beforeEach(() => { + sut = new PsLogEntryFormatter() + }) + + test('returns formatted string', () => { + const message: IPsReportLogEntry = { + generatedAt: moment('2022-03-18 14:43:02'), + message: 'foo-bar', + source: PsReportSource.PupilGenerator, + level: 'info' + } + const output = sut.formatEntry(message) + const expectedOutput = `${message.generatedAt.toISOString()}: [${message.source}] ${message.level} - ${message.message}` + expect(output).toStrictEqual(expectedOutput) + }) +}) diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry-formatter.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry-formatter.ts new file mode 100644 index 0000000000..b2bf15cb31 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry-formatter.ts @@ -0,0 +1,9 @@ +import { IPsReportLogEntry } from '../common/ps-report-log-entry' +import moment from 'moment' +export class PsLogEntryFormatter { + formatEntry (message: IPsReportLogEntry): string { + const m = moment(message.generatedAt) + const formatted = `${m.toISOString()}: [${message.source}] ${message.level} - ${message.message}` + return formatted + } +} diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry.converter.spec.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry.converter.spec.ts new file mode 100644 index 0000000000..9422b7232f --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry.converter.spec.ts @@ -0,0 +1,25 @@ +import { PsLogEntryConverter } from './log-entry.converter' + +let sut: PsLogEntryConverter + +describe('log entry converter', () => { + beforeEach(() => { + sut = new PsLogEntryConverter() + }) + + test('subject should be defined', () => { + expect(sut).toBeDefined() + }) + + test('if no entries, emtpy buffer is returned', () => { + const input = new Array() + const output = sut.convert(input) + expect(output).not.toBeDefined() + }) + + test('entries should be prepended with a new line', () => { + const input = ['foo'] + const output = sut.convert(input) + expect(output?.toString()).toStrictEqual('\nfoo') + }) +}) diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry.converter.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry.converter.ts new file mode 100644 index 0000000000..65e6d69a2e --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-entry.converter.ts @@ -0,0 +1,6 @@ +export class PsLogEntryConverter { + convert (entries: string[]): Buffer | undefined { + if (entries.length === 0) return + return Buffer.from(`\n${entries.join('\n')}`) + } +} diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log-generator.service.spec.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-generator.service.spec.ts new file mode 100644 index 0000000000..a7eaccc162 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-generator.service.spec.ts @@ -0,0 +1,50 @@ +import moment from 'moment' +import { IPsReportLogEntry, PsReportSource } from '../common/ps-report-log-entry' +import { PsLogEntryFormatter } from './log-entry-formatter' +import { PsLogSetGeneratorService } from './log-generator.service' +import { IPsReportLogSetBatch } from './ps-report-log-set' + +let sut: PsLogSetGeneratorService +let formatter: PsLogEntryFormatter + +describe('log generator service', () => { + beforeEach(() => { + sut = new PsLogSetGeneratorService() + formatter = new PsLogEntryFormatter() + }) + + test('subject is defined', () => { + expect(sut).toBeDefined() + }) + + test('it returns log set containing formatted messages', () => { + const setId = 'foo-bar' + const pupilGeneratorMessage: IPsReportLogEntry = { + generatedAt: moment('2022-03-10 12:00:00'), + message: 'foo', + source: PsReportSource.PupilGenerator, + level: 'error' + } + const transformerMessage: IPsReportLogEntry = { + generatedAt: moment('2022-03-10 12:00:03'), + message: 'bar', + source: PsReportSource.Transformer, + level: 'info' + } + const writerMessage: IPsReportLogEntry = { + generatedAt: moment('2022-03-10 12:11:00'), + message: 'qux', + source: PsReportSource.Writer, + level: 'warning' + } + const expected: IPsReportLogSetBatch = { + setId: setId, + listSchoolsLog: [], + pupilDataLog: [formatter.formatEntry(pupilGeneratorMessage)], + transformerLog: [formatter.formatEntry(transformerMessage)], + writerLog: [formatter.formatEntry(writerMessage)] + } + const actual = sut.generate(setId, [pupilGeneratorMessage, transformerMessage, writerMessage]) + expect(actual).toStrictEqual(expected) + }) +}) diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log-generator.service.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-generator.service.ts new file mode 100644 index 0000000000..3ca748902b --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-generator.service.ts @@ -0,0 +1,42 @@ +import { IPsReportLogEntry, PsReportSource } from '../common/ps-report-log-entry' +import { IPsReportLogSetBatch } from './ps-report-log-set' +import { PsLogEntryFormatter } from './log-entry-formatter' + +export class PsLogSetGeneratorService { + private readonly formatter = new PsLogEntryFormatter() + private readonly listSchoolsLog = new Array() + private readonly pupilDataLog = new Array() + private readonly transformerLog = new Array() + private readonly writerLog = new Array() + + generate (setId: string | undefined, entries: IPsReportLogEntry[]): IPsReportLogSetBatch { + if (setId === undefined) throw new Error('setId required') + for (let index = 0; index < entries.length; index++) { + const entry = entries[index] + const formattedEntry = this.formatter.formatEntry(entry) + switch (entry.source) { + case PsReportSource.PupilGenerator: + this.pupilDataLog.push(formattedEntry) + break + case PsReportSource.SchoolGenerator: + this.listSchoolsLog.push(formattedEntry) + break + case PsReportSource.Transformer: + this.transformerLog.push(formattedEntry) + break + case PsReportSource.Writer: + this.writerLog.push(formattedEntry) + break + default: + throw new Error(`unhandled function source: ${entry.source}`) + } + } + return { + setId: setId, + listSchoolsLog: this.listSchoolsLog, + pupilDataLog: this.pupilDataLog, + transformerLog: this.transformerLog, + writerLog: this.writerLog + } + } +} diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log-writer.spec.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-writer.spec.ts new file mode 100644 index 0000000000..89f3445139 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-writer.spec.ts @@ -0,0 +1,57 @@ +import { IBlobService } from '../../azure/blob-service' +import { IPsReportLogSetBatch } from './ps-report-log-set' +import { PsLogWriter, LogContainerPrefix } from './log-writer' + +let sut: PsLogWriter +const DataServiceMock = jest.fn(() => ({ + createBlob: jest.fn(), + deleteBlob: jest.fn(), + appendBlob: jest.fn() +})) + +let dataService: IBlobService + +describe('ps report log writer', () => { + beforeEach(() => { + dataService = new DataServiceMock() + sut = new PsLogWriter(dataService) + }) + + test('subject should be defined', () => { + expect(sut).toBeDefined() + }) + + test('it should write each log file to the same blob storage container', async () => { + const logSet: IPsReportLogSetBatch = { + setId: '[the-set-id]', + listSchoolsLog: ['sdfsdfdsfsdfsd', 'erh43h5ehergdr', 'rdgfds', 'xxxdf'], + pupilDataLog: ['sldfjdslkfj', '3408gwaehiow4ty8'], + transformerLog: ['wouergh', 'o8324thof48g4', '3o84ghegopijey4p9u'], + writerLog: ['sldfjsdklfj'] + } + const expectedListSchoolsFileName = `list-schools-log-${logSet.setId}.txt` + const expectedPupilDataFileName = `pupil-data-log-${logSet.setId}.txt` + const expectedTransformerFileName = `transformer-log-${logSet.setId}.txt` + const expectedWriterFileName = `writer-log-${logSet.setId}.txt` + const expectedContainerName = `${LogContainerPrefix}-${logSet.setId}` + + await sut.writeToStorage(logSet) + expect(dataService.appendBlob).toHaveBeenCalledTimes(4) + expect(dataService.appendBlob).toHaveBeenNthCalledWith(1, Buffer.from(`\n${logSet.listSchoolsLog.join('\n')}`), expectedListSchoolsFileName, expectedContainerName) + expect(dataService.appendBlob).toHaveBeenNthCalledWith(2, Buffer.from(`\n${logSet.pupilDataLog.join('\n')}`), expectedPupilDataFileName, expectedContainerName) + expect(dataService.appendBlob).toHaveBeenNthCalledWith(3, Buffer.from(`\n${logSet.transformerLog.join('\n')}`), expectedTransformerFileName, expectedContainerName) + expect(dataService.appendBlob).toHaveBeenNthCalledWith(4, Buffer.from(`\n${logSet.writerLog.join('\n')}`), expectedWriterFileName, expectedContainerName) + }) + + test('it should not attempt to append blob data if a set item is empty', async () => { + const logSet: IPsReportLogSetBatch = { + setId: '[the-set-id]', + listSchoolsLog: [], + pupilDataLog: ['sldfjdslkfj', '3408gwaehiow4ty8'], + transformerLog: ['wouergh', 'o8324thof48g4', '3o84ghegopijey4p9u'], + writerLog: ['sldfjsdklfj'] + } + await sut.writeToStorage(logSet) + expect(dataService.appendBlob).toHaveBeenCalledTimes(3) + }) +}) diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log-writer.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-writer.ts new file mode 100644 index 0000000000..558648648d --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log-writer.ts @@ -0,0 +1,38 @@ +import { IPsReportLogSetBatch } from './ps-report-log-set' +import { BlobService, IBlobService } from '../../azure/blob-service' +import { PsLogEntryConverter } from './log-entry.converter' +export interface IPsLogWriter { + writeToStorage (logSet: IPsReportLogSetBatch): Promise +} + +export const LogContainerPrefix = 'ps-report-log' +export class PsLogWriter implements IPsLogWriter { + private readonly dataService: IBlobService + private readonly entryConverter = new PsLogEntryConverter() + + constructor (dataService?: IBlobService) { + this.dataService = dataService ?? new BlobService() + } + + async writeToStorage (logSet: IPsReportLogSetBatch): Promise { + const listSchoolsBuffer = this.entryConverter.convert(logSet.listSchoolsLog) + const pupilDataBuffer = this.entryConverter.convert(logSet.pupilDataLog) + const transformerBuffer = this.entryConverter.convert(logSet.transformerLog) + const writerBuffer = this.entryConverter.convert(logSet.writerLog) + + const containerName = `${LogContainerPrefix}-${logSet.setId}` + + const listSchoolsFileName = `list-schools-log-${logSet.setId}.txt` + const pupilDataFileName = `pupil-data-log-${logSet.setId}.txt` + const transformerFileName = `transformer-log-${logSet.setId}.txt` + const writerFileName = `writer-log-${logSet.setId}.txt` + + const promises = new Array>() + if (listSchoolsBuffer !== undefined) promises.push(this.dataService.appendBlob(listSchoolsBuffer, listSchoolsFileName, containerName)) + if (pupilDataBuffer !== undefined) promises.push(this.dataService.appendBlob(pupilDataBuffer, pupilDataFileName, containerName)) + if (transformerBuffer !== undefined) promises.push(this.dataService.appendBlob(transformerBuffer, transformerFileName, containerName)) + if (writerBuffer !== undefined) promises.push(this.dataService.appendBlob(writerBuffer, writerFileName, containerName)) + + await Promise.all(promises) + } +} diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log.service.spec.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log.service.spec.ts new file mode 100644 index 0000000000..4f68740240 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log.service.spec.ts @@ -0,0 +1,63 @@ +import moment from 'moment' +import { IPsReportLogEntry, PsReportSource } from '../common/ps-report-log-entry' +import { IPsLogWriter } from './log-writer' +import { LogService } from './log.service' +import { IServiceBusMessageLike } from './service-bus-message-like' + +const LogWriterMock = jest.fn(() => ({ + writeToStorage: jest.fn() +})) + +let sut: LogService +let logWriter: IPsLogWriter + +const entries: IPsReportLogEntry[] = [ + { + generatedAt: moment('2021-12-15 18:43:12'), + message: 'this is a test message', + source: PsReportSource.PupilGenerator, + level: 'info' + }, + { + generatedAt: moment('2021-12-15 18:44:02'), + message: 'this is a test message', + source: PsReportSource.SchoolGenerator, + level: 'error' + }, + { + generatedAt: moment('2021-12-15 18:45:11'), + message: 'this is a test message', + source: PsReportSource.Transformer, + level: 'verbose' + }, + { + generatedAt: moment('2021-12-15 18:45:19'), + message: 'this is a test message', + source: PsReportSource.Writer, + level: 'warning' + } +] + +const messages: IServiceBusMessageLike[] = entries.map(e => { + return { + body: e + } +}) + +describe('log service', () => { + beforeEach(() => { + logWriter = new LogWriterMock() + sut = new LogService(logWriter) + }) + + test('subject is defined', () => { + expect(sut).toBeDefined() + }) + + test('orchestrates parsing, formatting and writing of log files', async () => { + const setId = 'foo-bar' + jest.spyOn(logWriter, 'writeToStorage').mockImplementation() + await sut.create(setId, messages) + expect(logWriter.writeToStorage).toHaveBeenCalledTimes(1) + }) +}) diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/log.service.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/log.service.ts new file mode 100644 index 0000000000..e39c9b88e7 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/log.service.ts @@ -0,0 +1,26 @@ +import { PsLogSetGeneratorService } from './log-generator.service' +import { IPsLogWriter, PsLogWriter } from './log-writer' +import { PsLogMessageParser } from './message-parser' +import { IServiceBusMessageLike } from './service-bus-message-like' + +export class LogService { + private readonly generator: PsLogSetGeneratorService + private readonly writer: IPsLogWriter + private readonly parser: PsLogMessageParser + + constructor (logWriter?: IPsLogWriter) { + this.generator = new PsLogSetGeneratorService() + this.parser = new PsLogMessageParser() + this.writer = logWriter ?? new PsLogWriter() + } + + async create (setId: string, messages: IServiceBusMessageLike[]): Promise { + // setId must be created and owned by caller + // create set container if not exists + // create set files if not exists + // append messages to set files + const entries = this.parser.parse(messages) + const set = this.generator.generate(setId, entries) + await this.writer.writeToStorage(set) + } +} diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/message-parser.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/message-parser.ts new file mode 100644 index 0000000000..81aa550357 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/message-parser.ts @@ -0,0 +1,11 @@ +import { IPsReportLogEntry } from '../common/ps-report-log-entry' +import { IServiceBusMessageLike } from './service-bus-message-like' + +export class PsLogMessageParser { + parse (messages: IServiceBusMessageLike[]): IPsReportLogEntry[] { + return messages.map(m => { + const entry: IPsReportLogEntry = m.body + return entry + }) + } +} diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/mock-log-entries.json b/tslib/src/functions-ps-report/ps-report-5-log-writer/mock-log-entries.json new file mode 100644 index 0000000000..c608b39c2a --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/mock-log-entries.json @@ -0,0 +1,22 @@ +[ + { + "message": "Test Message", + "source": "School Generator", + "generatedAt": "2022-03-17 15:02:34" + }, + { + "message": "Test Message", + "source": "Pupil Generator", + "generatedAt": "2022-03-17 15:04:26" + }, + { + "message": "Test Message", + "source": "Transformer", + "generatedAt": "2022-03-17 15:06:10" + }, + { + "message": "Test Message", + "source": "Writer", + "generatedAt": "2022-03-17 15:06:29" + } +] diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/ps-report-log-set.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/ps-report-log-set.ts new file mode 100644 index 0000000000..fab9d58325 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/ps-report-log-set.ts @@ -0,0 +1,7 @@ +export interface IPsReportLogSetBatch { + setId: string + listSchoolsLog: string[] + pupilDataLog: string[] + transformerLog: string[] + writerLog: string[] +} diff --git a/tslib/src/functions-ps-report/ps-report-5-log-writer/service-bus-message-like.ts b/tslib/src/functions-ps-report/ps-report-5-log-writer/service-bus-message-like.ts new file mode 100644 index 0000000000..04d45bb239 --- /dev/null +++ b/tslib/src/functions-ps-report/ps-report-5-log-writer/service-bus-message-like.ts @@ -0,0 +1,4 @@ + +export interface IServiceBusMessageLike { + body: any +} diff --git a/tslib/src/functions-throttled/census-import/v1.spec.ts b/tslib/src/functions-throttled/census-import/v1.spec.ts index 46bf8c2d0d..6b7db7c9cc 100644 --- a/tslib/src/functions-throttled/census-import/v1.spec.ts +++ b/tslib/src/functions-throttled/census-import/v1.spec.ts @@ -22,7 +22,9 @@ const JobDataServiceMock = jest.fn(() => ({ })) const BlobServiceMock = jest.fn(() => ({ - deleteBlob: jest.fn() + deleteBlob: jest.fn(), + createBlob: jest.fn(), + appendBlob: jest.fn() })) const LoggerMock = jest.fn(() => ({ diff --git a/tslib/src/functions/util-gen-ps-report-logs/index.ts b/tslib/src/functions/util-gen-ps-report-logs/index.ts new file mode 100644 index 0000000000..5232df770f --- /dev/null +++ b/tslib/src/functions/util-gen-ps-report-logs/index.ts @@ -0,0 +1,69 @@ +import { AzureFunction, Context, HttpRequest } from '@azure/functions' +import moment from 'moment' +import { performance } from 'perf_hooks' +import config from '../../config' +import { IPsReportLogEntry, PsReportSource } from '../../functions-ps-report/common/ps-report-log-entry' +import * as uuid from 'uuid' + +const functionName = 'util-gen-ps-report-logs' + +function finish (start: number, context: Context): void { + const end = performance.now() + const durationInMilliseconds = end - start + const timeStamp = new Date().toISOString() + context.log(`${functionName}: ${timeStamp} run complete: ${durationInMilliseconds} ms`) +} + +const funcImplementation: AzureFunction = async function (context: Context, req: HttpRequest): Promise { + if (!config.DevTestUtils.TestSupportApi) { + context.log('exiting as not enabled (default behaviour)') + context.done() + return + } + const start = performance.now() + let messageCount = 10000 + if (req.query.messageCount !== undefined) { + messageCount = parseInt(req.query.messageCount, 10) + } + context.log(`attempting to add ${messageCount} messages onto the ps-report-log service bus queue...`) + const messages: IPsReportLogEntry[] = [] + for (let index = 0; index < messageCount; index++) { + const batch: IPsReportLogEntry[] = [ + { + generatedAt: moment(), + message: `${uuid.v4()} this is a test message`, + source: PsReportSource.PupilGenerator, + level: 'error' + }, + { + generatedAt: moment(), + message: `${uuid.v4()} this is a test message`, + source: PsReportSource.SchoolGenerator, + level: 'error' + }, + { + generatedAt: moment(), + message: `${uuid.v4()} this is a test message`, + source: PsReportSource.Transformer, + level: 'info' + }, + { + generatedAt: moment(), + message: `${uuid.v4()} this is a test message`, + source: PsReportSource.Writer, + level: 'verbose' + } + ] + messages.push(...batch) + } + context.bindings.psReportLogQueue = messages + context.log(`finished adding ${messageCount} messages onto the ps-report-log service bus queue...`) + finish(start, context) + context.res = { + status: 204, + body: '' + } + context.done() +} + +export default funcImplementation